erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md
rckrdmrd 7f422e51db
Some checks failed
CI Pipeline / Lint & Type Check (push) Has been cancelled
CI Pipeline / Validate SSOT Constants (push) Has been cancelled
CI Pipeline / Backend Tests (push) Has been cancelled
CI Pipeline / Frontend Tests (push) Has been cancelled
CI Pipeline / Build (push) Has been cancelled
CI Pipeline / Docker Build (push) Has been cancelled
feat: Documentation and orchestration updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:35:28 -06:00

53 KiB
Raw Blame History

RF-AUTH-002: Estados de Cuenta de Usuario

📋 Metadata

Campo Valor
ID RF-AUTH-002
Épica MAI-001 - Fundamentos
Módulo Autenticación y Autorización
Prioridad Alta
Estado 🚧 Planificado
Versión 1.0
Fecha creación 2025-11-17
Última actualización 2025-11-17
Esfuerzo estimado 12h (vs 15h GAMILIT - 20% ahorro por reutilización)
Story Points 5 SP

🔗 Referencias

Especificación Técnica

📐 ET-AUTH-002: Gestión de Estados de Cuenta (Pendiente)

♻️ Reutilización: 85%

  • Catálogo de referencia: shared/catalog/auth/ (Patrón estados de cuenta)
  • Diferencias clave:
    • Estados adaptados a contexto de construcción
    • Casos de uso específicos para roles de obra
    • Integración con sistema de constructoras

Implementación DDL

🗄️ ENUM Canónico:

-- Location: apps/database/ddl/00-prerequisites.sql
CREATE TYPE auth_management.user_status AS ENUM (
  'active',       -- Usuario verificado, acceso completo
  'inactive',     -- Usuario desactivó su cuenta temporalmente
  'suspended',    -- Admin suspendió cuenta (reversible)
  'banned',       -- Admin baneó cuenta (permanente)
  'pending'       -- Email no verificado
);

🗄️ Tablas que usan el ENUM:

  1. auth_management.profiles

    • Columna: status auth_management.user_status NOT NULL DEFAULT 'pending'
  2. auth_management.user_constructoras

    • Columna: status auth_management.user_status NOT NULL DEFAULT 'active'
    • Nota: Permite diferentes estados por constructora

🗄️ Funciones:

-- Validar estado del usuario
CREATE FUNCTION auth_management.verify_user_status(
  p_user_id UUID,
  p_constructora_id UUID
) RETURNS BOOLEAN;

-- Suspender usuario
CREATE FUNCTION auth_management.suspend_user(
  p_user_id UUID,
  p_reason TEXT,
  p_duration_days INTEGER,
  p_suspended_by UUID
) RETURNS VOID;

-- Banear usuario permanentemente
CREATE FUNCTION auth_management.ban_user(
  p_user_id UUID,
  p_reason TEXT,
  p_banned_by UUID
) RETURNS VOID;

-- Reactivar usuario (desde inactive o suspended)
CREATE FUNCTION auth_management.reactivate_user(
  p_user_id UUID,
  p_reactivated_by UUID
) RETURNS VOID;

🗄️ Triggers:

-- Auditar cambios de estado
CREATE TRIGGER trg_profiles_status_change
    AFTER UPDATE OF status ON auth_management.profiles
    FOR EACH ROW
    WHEN (OLD.status IS DISTINCT FROM NEW.status)
    EXECUTE FUNCTION audit_logging.log_status_change();

Backend

💻 Implementación:

  • Enum: apps/backend/src/modules/auth/enums/user-status.enum.ts
export enum UserStatus {
  ACTIVE = 'active',
  INACTIVE = 'inactive',
  SUSPENDED = 'suspended',
  BANNED = 'banned',
  PENDING = 'pending',
}
  • Middleware: apps/backend/src/modules/auth/middleware/user-status.middleware.ts
  • Service: apps/backend/src/modules/auth/services/user-management.service.ts
  • DTOs:
    • apps/backend/src/modules/auth/dto/update-user-status.dto.ts
    • apps/backend/src/modules/auth/dto/suspend-user.dto.ts

Frontend

🎨 Componentes:

  • Types: apps/frontend/src/types/auth.types.ts
interface UserProfile {
  id: string;
  email: string;
  status: UserStatus;
  role: ConstructionRole;
  constructoraId: string;
  // ...otros campos
}
  • Componentes:
    • apps/frontend/src/components/ui/UserStatusBadge.tsx - Badge visual del estado
    • apps/frontend/src/features/admin/UserManagementPanel.tsx - Panel de gestión
    • apps/frontend/src/features/admin/SuspendUserModal.tsx - Modal para suspender
    • apps/frontend/src/features/auth/AccountStatusPage.tsx - Página de estado de cuenta

Trazabilidad

📊 TRACEABILITY.yml


📝 Descripción del Requerimiento

Contexto

En un sistema de gestión de obra, las cuentas de usuario requieren un control riguroso del ciclo de vida completo:

  • Verificación inicial: Usuario invitado debe verificar su email antes de acceder
  • Seguridad: Suspender cuentas comprometidas o con comportamiento inadecuado
  • Compliance: Dar de baja usuarios que ya no trabajan en la constructora
  • Flexibilidad: Permitir desactivación temporal voluntaria
  • Multi-tenancy: Un usuario puede tener diferentes estados en diferentes constructoras

Necesidad del Negocio

Problema: Sin un sistema de estados bien definido:

  • No se puede verificar email antes de dar acceso a datos sensibles de obra
  • No hay forma de suspender temporalmente usuarios problemáticos
  • No se puede diferenciar entre baja temporal (inactivo) y baja permanente (baneado)
  • Empleados que ya no trabajan siguen teniendo acceso a información confidencial
  • No hay auditoría de cambios de estado

Solución: Implementar un sistema de 5 estados que modela el ciclo de vida completo, con transiciones controladas, auditadas y específicas para construcción.


🎯 Requerimiento Funcional

RF-AUTH-002.1: Estados Disponibles

El sistema DEBE soportar exactamente 5 estados de cuenta:

1. Pendiente (pending)

Descripción: Cuenta recién creada por invitación, email no verificado

Características:

  • Estado inicial al ser invitado a una constructora
  • Usuario no puede acceder al sistema
  • Se envía email de verificación automáticamente
  • ⏱️ Expira después de 7 días si no se verifica
  • 🔒 No tiene acceso a datos de obra

Acceso Permitido:

  • Dashboard
  • Proyectos/Obras
  • Módulos del sistema
  • Página de verificación de email
  • Reenviar email de verificación

Transiciones:

  • active: Al verificar email exitosamente
  • → ∅ (eliminación automática): Después de 7 días sin verificar

Caso de Uso Típico:

Residente de Obra es invitado por el Director. Recibe email, tiene 7 días para verificar antes de que la invitación expire.


2. Activo (active)

Descripción: Cuenta verificada, acceso completo según rol asignado

Características:

  • Email verificado exitosamente
  • Acceso completo según rol (director, engineer, resident, etc.)
  • Puede usar app móvil de asistencias
  • Recibe notificaciones del sistema
  • Aparece en búsquedas de usuarios

Acceso Permitido:

  • Todo el sistema según permisos de rol
  • Dashboard personalizado por rol
  • Módulos asignados (proyectos, presupuestos, RRHH, etc.)
  • App móvil (si tiene rol de resident o hr)

Transiciones:

  • inactive: Usuario desactiva su propia cuenta
  • suspended: Admin suspende la cuenta (reversible)
  • banned: Admin banea la cuenta (irreversible)

Caso de Uso Típico:

Ingeniero accede diariamente para revisar presupuestos, actualizar programación y aprobar requisiciones.


3. Inactivo (inactive)

Descripción: Usuario desactivó temporalmente su cuenta (voluntario)

Características:

  • 🔵 Desactivación voluntaria por el usuario
  • 💾 Datos conservados intactos
  • 🔄 Reversible por el propio usuario en cualquier momento
  • 📧 No recibe notificaciones
  • 🚫 No aparece en búsquedas de usuarios

Acceso Permitido:

  • Dashboard y funcionalidades principales
  • Módulos del sistema
  • Página de reactivación de cuenta
  • Descargar datos personales (GDPR compliance)

Transiciones:

  • active: Usuario reactiva su cuenta (máximo 3 veces por día)
  • → ∅ (eliminación voluntaria): Usuario solicita eliminación de cuenta

Caso de Uso Típico:

Residente que toma vacaciones de 2 semanas desactiva temporalmente su cuenta para no recibir notificaciones.

Rate Limiting:

  • Máximo 3 reactivaciones por día (prevenir abuso)

4. Suspendido (suspended)

Descripción: Admin suspendió la cuenta (reversible)

Características:

  • 🔴 Suspensión por admin debido a comportamiento inapropiado
  • 🔄 REVERSIBLE (diferencia clave con banned)
  • 📝 Requiere razón documentada obligatoria
  • 🔍 Requiere revisión de admin para levantar suspensión
  • 📬 Usuario recibe notificación con razón de suspensión

Acceso Permitido:

  • Todo el sistema (bloqueado completamente)
  • Ver notificación de suspensión con razón
  • Contactar soporte

Restricciones:

  • 🚫 No puede iniciar sesión
  • 📧 No recibe notificaciones del sistema
  • 🔍 No aparece en búsquedas de usuarios
  • 📱 App móvil bloqueada

Transiciones:

  • active: Admin levanta suspensión después de revisión
  • banned: Admin decide hacer baneo permanente

Duración:

  • Típicamente: 7-30 días
  • Requiere revisión periódica por directores/admins

Razones Comunes (Construcción):

  • Registro de asistencia fraudulento (check-in sin estar en obra)
  • Captura incorrecta de avances de obra
  • Modificación no autorizada de presupuestos
  • Comportamiento inapropiado en obra

Caso de Uso Típico:

Residente registró asistencias falsas de empleados. Director lo suspende 14 días mientras investiga.


5. Baneado (banned)

Descripción: Admin baneó la cuenta permanentemente (irreversible)

Características:

  • 🔴 Baneo por violación grave de términos
  • IRREVERSIBLE (no hay transición de vuelta)
  • 📧 Email y username quedan bloqueados permanentemente
  • 💾 Datos se conservan por auditoría pero cuenta inaccesible
  • 📝 Requiere razón grave documentada

Acceso Permitido:

  • Todo el sistema
  • Ver notificación de baneo con razón

Restricciones:

  • 🚫 No puede iniciar sesión
  • 🚫 No puede crear nueva cuenta con mismo email
  • 🚫 Username queda reservado permanentemente
  • 🚫 No puede ser invitado nuevamente a ninguna constructora

Razones Comunes (Construcción):

  • Fraude financiero (apropiación indebida de recursos)
  • Robo de información confidencial de la constructora
  • Suplantación de identidad (firmar como otro usuario)
  • Alteración de documentos oficiales (contratos, presupuestos)
  • Actividad criminal relacionada con la obra

Caso de Uso Típico:

Empleado de compras desvió recursos, realizó órdenes de compra falsas. Director lo banea permanentemente y procede legalmente.


RF-AUTH-002.2: Flujo de Estados

┌──────────────────────┐
│   INVITACIÓN A       │
│   CONSTRUCTORA       │
└──────────┬───────────┘
           │
           ▼
       pending ──────(verificar email)─────> active
           │                                   │
           │                                   ├──(usuario desactiva)──> inactive
           │                                   │                            │
           │                                   │                            └──(reactiva)──> active
           │                                   │
           │                                   ├──(admin suspende)──> suspended
           │                                   │                         │
           │                                   │                         ├──(admin levanta)──> active
           │                                   │                         └──(admin decide)──> banned
           │                                   │
           └────(7 días)───> ∅                 └──(admin banea)──> banned
            (eliminación)                                              │
                                                                      └──(permanente)──> ∅

Leyenda:

  • ∅ = Registro eliminado/cuenta no recuperable
  • Flechas sólidas = Transiciones permitidas
  • Flechas punteadas = Eliminación automática

RF-AUTH-002.3: Reglas de Transición

Transiciones Permitidas

Estado Actual Puede Transicionar A Quién Puede Hacerlo Requiere
pending active Usuario Verificar email (click en link)
pending ∅ (eliminación) Sistema 7 días sin verificar
active inactive Usuario Confirmación + password
active suspended director, super_admin Razón documentada obligatoria
active banned director, super_admin Razón grave documentada + evidencia
inactive active Usuario Click en reactivar (máx 3/día)
suspended active director, super_admin Revisión completada + justificación
suspended banned director, super_admin Decisión justificada + evidencia
banned N/A Irreversible

Transiciones Prohibidas

Transición Razón
pendingsuspended No tiene sentido suspender cuenta no verificada (simplemente eliminar invitación)
pendingbanned No tiene sentido banear cuenta no verificada
inactivesuspended Usuario ya desactivó voluntariamente, no hay necesidad de suspender
inactivebanned Si se requiere baneo, reactivar primero y luego banear desde active
suspendedinactive Confusión de estados (uno es admin-driven, otro user-driven)
banned → cualquier otro Irreversible por diseño

RF-AUTH-002.4: Validación de Estado en Sistema Multi-tenancy

Importante: En este sistema, un usuario puede pertenecer a múltiples constructoras con diferentes estados en cada una.

Escenario Multi-tenancy

// Usuario puede tener diferentes estados en diferentes constructoras
const userConstructoras = [
  { constructoraId: 'A', status: 'active',    role: 'director' },
  { constructoraId: 'B', status: 'suspended', role: 'engineer' },
  { constructoraId: 'C', status: 'active',    role: 'resident' },
];

// Al hacer login, usuario ve solo constructoras donde status = 'active'
const availableConstructoras = userConstructoras.filter(
  uc => uc.status === 'active'
); // ['A', 'C']

1. Validación en Login

// POST /api/auth/login
// Retorna solo constructoras donde user.status = 'active'

async login(email: string, password: string) {
  const user = await this.findByEmail(email);

  // Validar estado global del perfil
  if (user.profile.status === 'banned') {
    throw new UnauthorizedException(
      'Tu cuenta ha sido baneada permanentemente. Contacta soporte para más información.'
    );
  }

  if (user.profile.status === 'pending') {
    throw new UnauthorizedException(
      'Debes verificar tu email antes de acceder. Revisa tu bandeja de entrada.'
    );
  }

  // Obtener constructoras donde el usuario está activo
  const activeConstructoras = await this.getActiveConstructoras(user.id);

  if (activeConstructoras.length === 0) {
    throw new UnauthorizedException(
      'No tienes acceso activo a ninguna constructora. Contacta al administrador.'
    );
  }

  return {
    user: user,
    accessToken: this.generateToken(user, activeConstructoras[0]),
    constructoras: activeConstructoras, // Usuario puede elegir
  };
}

2. Middleware en Cada Request

// UserStatusMiddleware valida en CADA request autenticado
@Injectable()
export class UserStatusMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    const user = req.user; // Incluye constructoraId del JWT

    // Excepciones: endpoints de reactivación y cambio de constructora
    const allowedPaths = [
      '/auth/reactivate',
      '/auth/status',
      '/auth/switch-constructora',
    ];
    if (allowedPaths.some(path => req.path.startsWith(path))) {
      return next();
    }

    // Validar estado global
    if (user.profileStatus === 'banned') {
      throw new ForbiddenException('Cuenta baneada permanentemente.');
    }

    if (user.profileStatus === 'pending') {
      throw new ForbiddenException('Email no verificado.');
    }

    // Validar estado en constructora actual
    if (user.constructoraStatus !== 'active') {
      throw new ForbiddenException(
        `Tu acceso a esta constructora está ${user.constructoraStatus}. ` +
        `Contacta al administrador o cambia a otra constructora.`
      );
    }

    next();
  }
}

3. Frontend - Validación Temprana

// Redirigir según estado
useEffect(() => {
  if (!user) return;

  // Estado global del perfil
  if (user.profile.status === 'pending') {
    navigate('/verify-email');
    return;
  }

  if (user.profile.status === 'banned') {
    navigate('/account-banned');
    return;
  }

  // Estado en constructora actual
  const currentConstructora = user.constructoras.find(
    c => c.id === user.currentConstructoraId
  );

  if (!currentConstructora) {
    navigate('/select-constructora');
    return;
  }

  if (currentConstructora.status === 'suspended') {
    navigate('/account-suspended-in-constructora');
    return;
  }

  if (currentConstructora.status === 'inactive') {
    navigate('/reactivate-account');
    return;
  }

  // Estado activo, continuar
}, [user, navigate]);

📊 Casos de Uso

UC-AUTH-003: Usuario verifica email tras invitación

Actor: Residente de Obra (nuevo usuario) Precondiciones:

  • Usuario invitado a constructora por Director
  • Email de invitación enviado
  • profiles.status = pending

Flujo Principal:

  1. Usuario recibe email de invitación a constructora "Constructora ABC"
  2. Usuario hace click en link de verificación
  3. Sistema valida token de verificación (válido por 24h)
  4. Sistema actualiza profiles.status de pendingactive
  5. Sistema actualiza user_constructoras.status de pendingactive
  6. Sistema audita cambio en audit_logs:
    {
      "action": "update",
      "resource_type": "user_status",
      "resource_id": "user_id",
      "details": {
        "old_status": "pending",
        "new_status": "active",
        "constructora_id": "abc-123",
        "verified_at": "2025-11-17T10:30:00Z"
      }
    }
    
  7. Sistema envía email de bienvenida con instrucciones
  8. Usuario redirigido a /auth/login
  9. Usuario hace login y selecciona constructora "Constructora ABC"
  10. Usuario accede a dashboard según su rol (resident)

Resultado: Usuario con status active puede acceder al sistema

Variantes:

  • V1: Token expirado (>24h):

    1. Sistema detecta token expirado
    2. Sistema muestra mensaje: "Link expirado"
    3. Sistema ofrece botón "Reenviar email de verificación"
    4. Usuario hace click
    5. Sistema envía nuevo email con nuevo token
    6. Volver a flujo principal paso 2
  • V2: Token inválido:

    1. Sistema detecta token inválido
    2. Sistema muestra error: "Link inválido"
    3. Sistema ofrece contactar soporte
    4. Sistema audita intento de verificación inválido
  • V3: Usuario ya verificado:

    1. Sistema detecta que status ya es active
    2. Sistema muestra: "Ya has verificado tu email"
    3. Sistema redirige a login

UC-ADMIN-002: Director suspende cuenta de Residente

Actor: Director de Construcción Precondiciones:

  • Usuario con rol director
  • Residente con status active
  • Comportamiento inapropiado detectado (ej: asistencias falsas)

Flujo Principal:

  1. Director navega a Panel de Gestión de Usuarios (/admin/users)
  2. Director filtra usuarios por constructora actual
  3. Director busca residente por nombre: "Juan Pérez"
  4. Director hace click en botón "Acciones" → "Suspender cuenta"
  5. Sistema muestra Modal de Suspensión con formulario:
    • Razón (textarea obligatorio, min 20 caracteres)
    • Duración sugerida (select: 7 días, 14 días, 30 días, Indefinido)
    • Evidencia (opcional: subir screenshots, documentos)
    • Fecha de revisión (automática según duración)
  6. Director completa formulario:
    • Razón: "Registró asistencias de empleados que no estaban en obra según GPS"
    • Duración: 14 días
    • Evidencia: Sube screenshots de GPS
  7. Director hace click en "Confirmar Suspensión"
  8. Sistema valida:
    • Razón tiene mínimo 20 caracteres
    • Usuario tiene permisos (rol = director)
    • Usuario a suspender no es director (no puede suspender otro director)
  9. Sistema ejecuta transacción:
    BEGIN;
    
    -- Actualizar estado
    UPDATE auth_management.user_constructoras
    SET status = 'suspended',
        suspended_at = NOW(),
        suspended_by = :director_id,
        suspended_reason = :reason,
        suspended_until = NOW() + INTERVAL '14 days'
    WHERE user_id = :resident_id
      AND constructora_id = :constructora_id;
    
    -- Auditar
    INSERT INTO audit_logging.audit_logs (
      action, resource_type, resource_id, performed_by, details
    ) VALUES (
      'update', 'user_status', :resident_id, :director_id,
      jsonb_build_object(
        'old_status', 'active',
        'new_status', 'suspended',
        'reason', :reason,
        'evidence_urls', :evidence_urls,
        'suspended_by_name', 'Director López',
        'review_date', NOW() + INTERVAL '14 days',
        'constructora_id', :constructora_id
      )
    );
    
    COMMIT;
    
  10. Sistema cierra todas las sesiones activas del residente en esa constructora
  11. Sistema envía Notificación Push + Email al residente:
    Asunto: Tu cuenta ha sido suspendida temporalmente
    
    Hola Juan,
    
    Tu cuenta en Constructora ABC ha sido suspendida por 14 días.
    
    Razón: Registró asistencias de empleados que no estaban en obra según GPS
    
    Fecha de revisión: 2025-12-01
    
    Si crees que esto es un error, puedes contactar a soporte en soporte@constructora.com
    
    ---
    Sistema de Gestión de Obra
    
  12. Sistema registra en timeline del usuario el evento de suspensión
  13. Sistema muestra confirmación al director: "Usuario suspendido exitosamente"

Resultado:

  • Residente no puede acceder a esa constructora por 14 días
  • Director puede revisar suspensión antes de la fecha
  • Suspensión queda auditada con evidencia

Variantes:

  • V1: Director intenta suspender otro director:

    • Sistema lanza error: "No puedes suspender a otro director"
    • Solo super_admin puede suspender directores
  • V2: Residente ya está suspendido:

    • Sistema detecta status = 'suspended'
    • Sistema muestra opciones:
      • Levantar suspensión
      • Extender suspensión
      • Convertir en baneo

Postcondiciones:

  • user_constructoras.status = 'suspended'
  • Registro en audit_logs
  • Email enviado
  • Sesiones cerradas

UC-AUTH-004: Usuario desactiva su propia cuenta

Actor: Ingeniero Precondiciones: Usuario con status active

Flujo Principal:

  1. Ingeniero navega a ConfiguraciónMi Cuenta (/settings/account)
  2. Ingeniero hace scroll hasta sección "Zona Peligrosa"
  3. Ingeniero hace click en botón "Desactivar mi cuenta"
  4. Sistema muestra Modal de Confirmación con:
    • Título: "¿Estás seguro que quieres desactivar tu cuenta?"
    • Explicación:
      • Esta acción es temporal
      • Tus datos NO se eliminarán
      • Puedes reactivar en cualquier momento
      • ⚠️ No recibirás notificaciones mientras esté desactivada
      • ⚠️ No aparecerás en búsquedas de usuarios
    • Campo: Contraseña (requerido para confirmar)
  5. Ingeniero ingresa su contraseña
  6. Ingeniero hace click en "Confirmar Desactivación"
  7. Sistema valida contraseña
  8. Sistema actualiza profiles.status de activeinactive
  9. Sistema audita cambio:
    {
      "action": "update",
      "resource_type": "user_status",
      "resource_id": "user_id",
      "details": {
        "old_status": "active",
        "new_status": "inactive",
        "deactivated_by": "self",
        "reason": "voluntary_deactivation",
        "deactivated_at": "2025-11-17T15:45:00Z"
      }
    }
    
  10. Sistema envía email de confirmación:
    Asunto: Cuenta desactivada temporalmente
    
    Hola,
    
    Tu cuenta ha sido desactivada exitosamente.
    
    Para reactivarla, simplemente haz login de nuevo en:
    https://app.constructora.com/auth/login
    
    Y haz click en "Reactivar cuenta".
    
    ---
    Sistema de Gestión de Obra
    
  11. Sistema cierra sesión del usuario (logout)
  12. Usuario redirigido a página de login con mensaje:

    "Tu cuenta ha sido desactivada. Puedes reactivarla en cualquier momento haciendo login."

Resultado: Cuenta desactivada temporalmente, usuario puede reactivar cuando quiera

Reactivación (Flujo Secundario):

  1. Usuario navega a /auth/login
  2. Usuario ingresa email + password
  3. Sistema detecta status = inactive
  4. Sistema muestra página de reactivación con:
    • Mensaje: "Tu cuenta está desactivada temporalmente"
    • Botón grande: "Reactivar mi cuenta"
  5. Usuario hace click en "Reactivar mi cuenta"
  6. Sistema valida rate limiting (máximo 3 reactivaciones por día)
  7. Sistema actualiza status de inactiveactive
  8. Sistema audita reactivación
  9. Sistema muestra: "Cuenta reactivada exitosamente"
  10. Usuario redirigido a dashboard

Postcondiciones:

  • profiles.status = 'inactive'
  • Email enviado
  • Sesión cerrada

UC-ADMIN-003: Director banea cuenta permanentemente

Actor: Director Precondiciones:

  • Usuario con rol director
  • Empleado de compras con status active o suspended
  • Violación grave detectada (fraude)

Flujo Principal:

  1. Director detecta fraude: Empleado de compras creó órdenes de compra falsas por $500,000 MXN
  2. Director navega a Panel de Gestión de Usuarios → Usuario "Carlos Ramírez"
  3. Director hace click en "Acciones" → "Banear cuenta permanentemente"
  4. Sistema muestra Modal de Baneo con advertencia:
    ⚠️ ADVERTENCIA: Esta acción es IRREVERSIBLE
    
    El usuario NO podrá:
    - Acceder al sistema nunca más
    - Crear nueva cuenta con este email
    - Ser invitado a ninguna constructora
    
    Esta acción debe reservarse para violaciones GRAVES:
    - Fraude financiero
    - Robo de información
    - Actividad criminal
    
    ¿Estás completamente seguro?
    
  5. Sistema requiere:
    • Razón grave (textarea obligatorio, min 50 caracteres)
    • Evidencia (obligatorio: mínimo 1 archivo)
    • Confirmación: Usuario debe escribir "BANEAR PERMANENTEMENTE"
  6. Director completa:
    • Razón: "Empleado creó órdenes de compra falsas por $500,000 MXN a proveedores ficticios. Se detectó desvío de recursos. Se procederá legalmente."
    • Evidencia: Sube PDFs de órdenes falsas, capturas de pantalla, reporte de auditoría
    • Confirmación: Escribe "BANEAR PERMANENTEMENTE"
  7. Director hace click en "Confirmar Baneo"
  8. Sistema ejecuta:
    BEGIN;
    
    -- Banear en todas las constructoras
    UPDATE auth_management.profiles
    SET status = 'banned',
        banned_at = NOW(),
        banned_by = :director_id,
        banned_reason = :reason
    WHERE id = :user_id;
    
    -- Marcar email como bloqueado
    INSERT INTO auth_management.banned_emails (email, reason, banned_by)
    VALUES (:user_email, :reason, :director_id);
    
    -- Auditar con máxima prioridad
    INSERT INTO audit_logging.audit_logs (
      action, resource_type, resource_id, performed_by, details, priority
    ) VALUES (
      'ban', 'user_status', :user_id, :director_id,
      jsonb_build_object(
        'old_status', 'active',
        'new_status', 'banned',
        'reason', :reason,
        'evidence_urls', :evidence_urls,
        'banned_by_name', 'Director López',
        'legal_action', true
      ),
      'critical'
    );
    
    -- Cerrar sesiones en TODAS las constructoras
    DELETE FROM auth_management.user_sessions
    WHERE user_id = :user_id;
    
    COMMIT;
    
  9. Sistema envía notificación CRÍTICA a:
    • Usuario baneado (email)
    • Todos los directores de todas las constructoras donde estaba el usuario
    • Super admins del sistema
  10. Sistema envía email al usuario baneado:
    Asunto: Cuenta baneada permanentemente
    
    Tu cuenta ha sido baneada permanentemente.
    
    Razón: [Razón documentada]
    
    Esta acción es irreversible. No podrás acceder al sistema ni crear nueva cuenta.
    
    Si tienes preguntas, contacta a legal@constructora.com
    

Resultado:

  • Usuario baneado en TODAS las constructoras
  • Email bloqueado permanentemente
  • Username reservado permanentemente
  • No puede ser invitado nunca más

Postcondiciones:

  • profiles.status = 'banned'
  • banned_emails contiene email
  • Sesiones cerradas en todas las constructoras
  • Notificaciones críticas enviadas

🔐 Consideraciones de Seguridad

1. Prevención de Bypass de Estado

Problema: Usuario suspendido podría intentar acceder vía API directamente, saltándose frontend

Solución: Middleware en CADA request autenticado

// apps/backend/src/modules/auth/middleware/user-status.middleware.ts
import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common';
import { UserStatus } from '../enums/user-status.enum';

@Injectable()
export class UserStatusMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    const user = req.user; // Inyectado por JwtAuthGuard

    // Excepciones: endpoints que permiten ciertos estados
    const allowedPaths = [
      '/auth/reactivate',           // inactive puede reactivar
      '/auth/status',               // consultar estado
      '/auth/switch-constructora',  // cambiar constructora
      '/auth/download-data',        // GDPR: inactive puede descargar datos
    ];

    if (allowedPaths.some(path => req.path.startsWith(path))) {
      return next();
    }

    // Validar estado global del perfil
    if (user.profileStatus === 'banned') {
      throw new ForbiddenException({
        statusCode: 403,
        message: 'Tu cuenta ha sido baneada permanentemente.',
        errorCode: 'ACCOUNT_BANNED',
        contactSupport: true,
      });
    }

    if (user.profileStatus === 'pending') {
      throw new ForbiddenException({
        statusCode: 403,
        message: 'Debes verificar tu email antes de acceder.',
        errorCode: 'EMAIL_NOT_VERIFIED',
        action: 'verify_email',
      });
    }

    // Validar estado en constructora actual
    if (user.constructoraStatus !== UserStatus.ACTIVE) {
      throw new ForbiddenException({
        statusCode: 403,
        message: `Tu acceso a esta constructora está ${user.constructoraStatus}.`,
        errorCode: 'CONSTRUCTORA_ACCESS_DENIED',
        constructoraId: user.constructoraId,
        status: user.constructoraStatus,
      });
    }

    next();
  }
}

Aplicación global:

// apps/backend/src/main.ts
app.use(UserStatusMiddleware); // Aplica a TODOS los endpoints

2. Auditoría Obligatoria de Cambios de Estado

Problema: Cambios de estado sin auditar = no hay trazabilidad

Solución: Trigger automático en base de datos

-- apps/database/ddl/schemas/auth_management/triggers/audit-status-change.sql

CREATE OR REPLACE FUNCTION audit_logging.log_status_change()
RETURNS TRIGGER AS $$
BEGIN
  -- Solo auditar si cambió el estado
  IF OLD.status IS DISTINCT FROM NEW.status THEN
    INSERT INTO audit_logging.audit_logs (
      action,
      resource_type,
      resource_id,
      performed_by,
      details,
      priority,
      ip_address
    ) VALUES (
      CASE NEW.status
        WHEN 'suspended' THEN 'suspend'
        WHEN 'banned' THEN 'ban'
        WHEN 'active' THEN 'reactivate'
        WHEN 'inactive' THEN 'deactivate'
        ELSE 'update'
      END,
      'user_status',
      NEW.id,
      COALESCE(current_setting('app.current_user_id', true)::UUID, NEW.id),
      jsonb_build_object(
        'old_status', OLD.status,
        'new_status', NEW.status,
        'reason', NEW.suspended_reason,
        'table', TG_TABLE_NAME,
        'timestamp', NOW()
      ),
      CASE NEW.status
        WHEN 'banned' THEN 'critical'
        WHEN 'suspended' THEN 'high'
        ELSE 'medium'
      END,
      inet_client_addr()
    );
  END IF;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Aplicar a profiles
CREATE TRIGGER trg_audit_status_change_profiles
    AFTER UPDATE OF status ON auth_management.profiles
    FOR EACH ROW
    EXECUTE FUNCTION audit_logging.log_status_change();

-- Aplicar a user_constructoras
CREATE TRIGGER trg_audit_status_change_constructoras
    AFTER UPDATE OF status ON auth_management.user_constructoras
    FOR EACH ROW
    EXECUTE FUNCTION audit_logging.log_status_change();

Resultado: TODO cambio de estado queda auditado automáticamente


3. Rate Limiting en Reactivación

Problema: Usuario abusa de desactivar/reactivar repetidamente

Solución: Limitar reactivaciones a 3 por día

// apps/backend/src/modules/auth/services/user-management.service.ts
import { TooManyRequestsException } from '@nestjs/common';

async reactivateAccount(userId: string): Promise<void> {
  // Contar reactivaciones hoy
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const reactivationCount = await this.auditLogRepository.count({
    where: {
      resourceId: userId,
      action: 'reactivate',
      createdAt: MoreThan(today),
    },
  });

  // Limitar a 3 reactivaciones por día
  if (reactivationCount >= 3) {
    throw new TooManyRequestsException({
      statusCode: 429,
      message: 'Has alcanzado el límite de reactivaciones por hoy (3 máximo).',
      errorCode: 'TOO_MANY_REACTIVATIONS',
      retryAfter: this.getSecondsUntilMidnight(),
    });
  }

  // Proceder con reactivación
  await this.profileRepository.update(
    { id: userId },
    { status: UserStatus.ACTIVE }
  );
}

private getSecondsUntilMidnight(): number {
  const now = new Date();
  const midnight = new Date(now);
  midnight.setHours(24, 0, 0, 0);
  return Math.floor((midnight.getTime() - now.getTime()) / 1000);
}

4. Notificación Obligatoria al Usuario

Problema: Usuario no sabe por qué cambió su estado

Solución: Notificación automática en todo cambio de estado

// apps/backend/src/modules/auth/services/user-status-notification.service.ts
import { Injectable } from '@nestjs/common';
import { NotificationService } from '../../notifications/notification.service';
import { EmailService } from '../../email/email.service';

@Injectable()
export class UserStatusNotificationService {
  constructor(
    private readonly notificationService: NotificationService,
    private readonly emailService: EmailService,
  ) {}

  async notifyStatusChange(
    userId: string,
    oldStatus: UserStatus,
    newStatus: UserStatus,
    reason: string,
    changedBy: string,
  ): Promise<void> {
    // Solo notificar si el cambio NO fue iniciado por el propio usuario
    if (changedBy !== userId) {
      // Notificación in-app (push)
      await this.notificationService.send({
        userId,
        type: 'system_announcement',
        priority: newStatus === 'banned' ? 'critical' : 'high',
        title: this.getNotificationTitle(newStatus),
        body: reason,
        icon: this.getNotificationIcon(newStatus),
        actions: this.getNotificationActions(newStatus),
      });

      // Email
      await this.emailService.send({
        to: await this.getUserEmail(userId),
        subject: this.getEmailSubject(newStatus),
        template: 'account-status-changed',
        data: {
          newStatus,
          oldStatus,
          reason,
          changedByName: await this.getUserName(changedBy),
          supportEmail: 'soporte@constructora.com',
        },
      });
    }
  }

  private getNotificationTitle(status: UserStatus): string {
    switch (status) {
      case UserStatus.SUSPENDED:
        return 'Tu cuenta ha sido suspendida temporalmente';
      case UserStatus.BANNED:
        return 'Tu cuenta ha sido baneada permanentemente';
      case UserStatus.ACTIVE:
        return 'Tu cuenta ha sido reactivada';
      case UserStatus.INACTIVE:
        return 'Tu cuenta ha sido desactivada';
      default:
        return 'Tu estado de cuenta ha cambiado';
    }
  }

  private getNotificationIcon(status: UserStatus): string {
    switch (status) {
      case UserStatus.SUSPENDED:
        return '⚠️';
      case UserStatus.BANNED:
        return '🚫';
      case UserStatus.ACTIVE:
        return '✅';
      case UserStatus.INACTIVE:
        return '';
      default:
        return '🔔';
    }
  }

  private getNotificationActions(status: UserStatus): any[] {
    switch (status) {
      case UserStatus.SUSPENDED:
        return [{ label: 'Contactar Soporte', action: 'contact_support' }];
      case UserStatus.BANNED:
        return [{ label: 'Ver Detalles', action: 'view_ban_details' }];
      case UserStatus.INACTIVE:
        return [{ label: 'Reactivar Cuenta', action: 'reactivate_account' }];
      default:
        return [];
    }
  }
}

5. Prevención de Registro con Email Baneado

Problema: Usuario baneado intenta crear nueva cuenta con mismo email

Solución: Validar email contra tabla banned_emails en registro

// apps/backend/src/modules/auth/services/auth.service.ts
async registerByInvitation(token: string, password: string): Promise<User> {
  const invitation = await this.validateInvitationToken(token);

  // Verificar si el email está baneado
  const isBanned = await this.bannedEmailRepository.findOne({
    where: { email: invitation.email },
  });

  if (isBanned) {
    throw new ForbiddenException({
      statusCode: 403,
      message: 'Este email está bloqueado y no puede crear una cuenta.',
      errorCode: 'EMAIL_BANNED',
      contactSupport: true,
      bannedReason: isBanned.reason,
    });
  }

  // Continuar con registro...
}
-- apps/database/ddl/schemas/auth_management/tables/banned-emails.sql
CREATE TABLE auth_management.banned_emails (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  email VARCHAR(255) UNIQUE NOT NULL,
  reason TEXT NOT NULL,
  banned_by UUID REFERENCES auth_management.profiles(id),
  banned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_banned_emails_email ON auth_management.banned_emails(email);

Criterios de Aceptación

AC-001: ENUM Implementado

  • ENUM user_status existe en schema auth_management
  • ENUM tiene exactamente 5 valores: active, inactive, suspended, banned, pending
  • Columna profiles.status usa el ENUM con default 'pending'
  • Columna user_constructoras.status usa el ENUM con default 'active'

AC-002: Transiciones Validadas

  • Solo transiciones permitidas pueden ejecutarse (validación en service)
  • Transiciones prohibidas lanzan exception con mensaje claro
  • Trigger audita TODOS los cambios de estado automáticamente
  • Razón es obligatoria para suspended y banned
  • Evidencia es obligatoria para banned

AC-003: Middleware Activo

  • UserStatusMiddleware aplicado globalmente en backend
  • Middleware valida estado en CADA request autenticado
  • Excepciones configuradas para paths específicos (/auth/reactivate, etc.)
  • Frontend redirige según status (pending → verify-email, banned → account-banned)
  • Usuario suspendido no puede acceder ni vía API ni vía UI

AC-004: Notificaciones Enviadas

  • Usuario recibe notificación push al cambiar status
  • Usuario recibe email con razón y próximos pasos
  • Notificación visible en UI con icono correcto
  • Email incluye información de contacto (soporte, legal)

AC-005: Admin Panel Funcional

  • Director puede suspender usuarios (excepto otros directores)
  • Director puede banear usuarios permanentemente
  • Razón es campo obligatorio (min 20 caracteres para suspend, 50 para ban)
  • Evidencia es obligatoria para baneo
  • Historial de cambios visible en perfil de usuario
  • Director puede levantar suspensión con justificación

AC-006: Usuario Puede Desactivar/Reactivar

  • Botón "Desactivar cuenta" visible en /settings/account
  • Modal de confirmación requiere contraseña
  • Reactivación simple desde login con botón "Reactivar cuenta"
  • Rate limiting: máximo 3 reactivaciones por día
  • Email de confirmación enviado al desactivar

AC-007: Multi-tenancy Soportado

  • Usuario puede tener diferentes estados en diferentes constructoras
  • Login muestra solo constructoras donde status = 'active'
  • Suspensión en una constructora no afecta otras constructoras
  • Baneo global afecta TODAS las constructoras

AC-008: Email Baneado Bloqueado

  • Tabla banned_emails creada
  • Registro valida email contra banned_emails
  • Email baneado no puede crear nueva cuenta
  • Error claro al intentar registro con email baneado

🧪 Testing

Test Suite: User Status Management

Test Case 1: Usuario pending no puede acceder

// apps/backend/src/modules/auth/tests/user-status.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, HttpStatus } from '@nestjs/common';
import * as request from 'supertest';

describe('UserStatusMiddleware - Pending Users', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('should block pending user from accessing protected endpoints', async () => {
    // Arrange: Crear usuario con status pending
    const user = await createTestUser({
      email: 'pending@test.com',
      status: UserStatus.PENDING,
    });
    const token = await generateAuthToken(user);

    // Act: Intentar acceder a dashboard
    const response = await request(app.getHttpServer())
      .get('/dashboard')
      .set('Authorization', `Bearer ${token}`)
      .expect(HttpStatus.FORBIDDEN);

    // Assert
    expect(response.body.message).toContain('verificar tu email');
    expect(response.body.errorCode).toBe('EMAIL_NOT_VERIFIED');
  });

  it('should allow pending user to access /auth/resend-verification', async () => {
    const user = await createTestUser({ status: UserStatus.PENDING });
    const token = await generateAuthToken(user);

    // Endpoint de excepción debe funcionar
    const response = await request(app.getHttpServer())
      .post('/auth/resend-verification')
      .set('Authorization', `Bearer ${token}`)
      .expect(HttpStatus.OK);

    expect(response.body.message).toContain('Email enviado');
  });
});

Test Case 2: Director puede suspender usuario

describe('UserManagementService - Suspend User', () => {
  it('should allow director to suspend resident with reason', async () => {
    // Arrange
    const director = await createTestUser({
      role: ConstructionRole.DIRECTOR,
      constructoraId: 'constructora-a',
    });
    const resident = await createTestUser({
      role: ConstructionRole.RESIDENT,
      status: UserStatus.ACTIVE,
      constructoraId: 'constructora-a',
    });

    await loginAs(director);

    // Act
    const response = await request(app.getHttpServer())
      .post(`/admin/users/${resident.id}/suspend`)
      .send({
        reason: 'Registró asistencias falsas de empleados',
        durationDays: 14,
        evidence: ['https://s3.aws.com/screenshot1.png'],
      })
      .expect(HttpStatus.OK);

    // Assert
    expect(response.body.message).toContain('suspendido exitosamente');

    // Verificar estado en DB
    const updatedResident = await getUserById(resident.id);
    expect(updatedResident.status).toBe(UserStatus.SUSPENDED);
    expect(updatedResident.suspendedReason).toBe('Registró asistencias falsas de empleados');

    // Verificar auditoría
    const auditLog = await getLatestAuditLog(resident.id, 'user_status');
    expect(auditLog.action).toBe('suspend');
    expect(auditLog.performedBy).toBe(director.id);
    expect(auditLog.details.reason).toBe('Registró asistencias falsas de empleados');
    expect(auditLog.priority).toBe('high');
  });

  it('should NOT allow director to suspend another director', async () => {
    const director1 = await createTestUser({ role: ConstructionRole.DIRECTOR });
    const director2 = await createTestUser({ role: ConstructionRole.DIRECTOR });

    await loginAs(director1);

    const response = await request(app.getHttpServer())
      .post(`/admin/users/${director2.id}/suspend`)
      .send({ reason: 'Test', durationDays: 7 })
      .expect(HttpStatus.FORBIDDEN);

    expect(response.body.message).toContain('No puedes suspender a otro director');
  });

  it('should require reason with minimum length', async () => {
    const director = await createTestUser({ role: ConstructionRole.DIRECTOR });
    const resident = await createTestUser({ role: ConstructionRole.RESIDENT });

    await loginAs(director);

    const response = await request(app.getHttpServer())
      .post(`/admin/users/${resident.id}/suspend`)
      .send({
        reason: 'Test', // Muy corto (< 20 caracteres)
        durationDays: 7,
      })
      .expect(HttpStatus.BAD_REQUEST);

    expect(response.body.message).toContain('Razón debe tener mínimo 20 caracteres');
  });
});

Test Case 3: Usuario puede desactivar y reactivar

describe('UserAccountService - Deactivate/Reactivate', () => {
  it('should allow user to deactivate their own account', async () => {
    // Arrange
    const user = await createTestUser({
      email: 'engineer@test.com',
      password: 'password123',
      status: UserStatus.ACTIVE,
    });
    await loginAs(user);

    // Act: Desactivar
    const deactivateResponse = await request(app.getHttpServer())
      .post('/auth/deactivate')
      .send({ password: 'password123' })
      .expect(HttpStatus.OK);

    expect(deactivateResponse.body.message).toContain('desactivada');

    // Assert: Verificar estado
    let updatedUser = await getUserById(user.id);
    expect(updatedUser.status).toBe(UserStatus.INACTIVE);

    // Assert: Verificar auditoría
    const auditLog = await getLatestAuditLog(user.id, 'user_status');
    expect(auditLog.action).toBe('deactivate');
    expect(auditLog.details.deactivatedBy).toBe('self');
  });

  it('should allow user to reactivate their account', async () => {
    // Arrange
    const user = await createTestUser({ status: UserStatus.INACTIVE });
    await loginAs(user);

    // Act: Reactivar
    const reactivateResponse = await request(app.getHttpServer())
      .post('/auth/reactivate')
      .expect(HttpStatus.OK);

    expect(reactivateResponse.body.message).toContain('reactivada');

    // Assert
    const updatedUser = await getUserById(user.id);
    expect(updatedUser.status).toBe(UserStatus.ACTIVE);
  });

  it('should enforce rate limiting on reactivation (max 3/day)', async () => {
    const user = await createTestUser({ status: UserStatus.INACTIVE });
    await loginAs(user);

    // Reactivar 3 veces (máximo permitido)
    for (let i = 0; i < 3; i++) {
      await request(app.getHttpServer())
        .post('/auth/reactivate')
        .expect(HttpStatus.OK);

      await request(app.getHttpServer())
        .post('/auth/deactivate')
        .send({ password: user.password })
        .expect(HttpStatus.OK);
    }

    // Intentar 4ta reactivación (debe fallar)
    const response = await request(app.getHttpServer())
      .post('/auth/reactivate')
      .expect(HttpStatus.TOO_MANY_REQUESTS);

    expect(response.body.errorCode).toBe('TOO_MANY_REACTIVATIONS');
    expect(response.body.message).toContain('límite de reactivaciones');
  });
});

Test Case 4: Banned user cannot login

describe('AuthService - Banned User', () => {
  it('should block banned user from logging in', async () => {
    // Arrange
    const user = await createTestUser({
      email: 'banned@test.com',
      password: 'password123',
      status: UserStatus.BANNED,
      bannedReason: 'Fraude financiero',
    });

    // Act
    const response = await request(app.getHttpServer())
      .post('/auth/login')
      .send({
        email: 'banned@test.com',
        password: 'password123',
      })
      .expect(HttpStatus.UNAUTHORIZED);

    // Assert
    expect(response.body.message).toContain('baneada permanentemente');
    expect(response.body.errorCode).toBe('ACCOUNT_BANNED');
    expect(response.body.contactSupport).toBe(true);
  });

  it('should block banned email from creating new account', async () => {
    // Arrange: Crear email baneado
    await createBannedEmail({
      email: 'fraud@test.com',
      reason: 'Usuario anterior cometió fraude',
    });

    // Act: Intentar registro con email baneado
    const invitationToken = await createInvitation('fraud@test.com');

    const response = await request(app.getHttpServer())
      .post('/auth/register-by-invitation')
      .send({
        token: invitationToken,
        password: 'newPassword123',
      })
      .expect(HttpStatus.FORBIDDEN);

    // Assert
    expect(response.body.errorCode).toBe('EMAIL_BANNED');
    expect(response.body.message).toContain('bloqueado');
  });
});

Test Case 5: Multi-tenancy status validation

describe('Multi-tenancy Status', () => {
  it('should allow user active in constructora A but suspended in B', async () => {
    // Arrange
    const user = await createTestUser({ email: 'multi@test.com' });

    await assignUserToConstructora(user.id, 'constructora-a', {
      status: UserStatus.ACTIVE,
      role: ConstructionRole.DIRECTOR,
    });

    await assignUserToConstructora(user.id, 'constructora-b', {
      status: UserStatus.SUSPENDED,
      role: ConstructionRole.ENGINEER,
    });

    // Act: Login
    const loginResponse = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email: 'multi@test.com', password: 'password' })
      .expect(HttpStatus.OK);

    // Assert: Solo debe ver constructora A (activa)
    expect(loginResponse.body.constructoras).toHaveLength(1);
    expect(loginResponse.body.constructoras[0].id).toBe('constructora-a');

    // Act: Acceder a constructora A (debe funcionar)
    const tokenA = loginResponse.body.accessToken;
    await request(app.getHttpServer())
      .get('/dashboard')
      .set('Authorization', `Bearer ${tokenA}`)
      .expect(HttpStatus.OK);

    // Act: Intentar cambiar a constructora B (debe fallar)
    const switchResponse = await request(app.getHttpServer())
      .post('/auth/switch-constructora')
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ constructoraId: 'constructora-b' })
      .expect(HttpStatus.FORBIDDEN);

    expect(switchResponse.body.message).toContain('suspended');
  });
});

Test Case 6: Trigger audits all status changes

describe('Audit Trigger', () => {
  it('should automatically audit status changes via trigger', async () => {
    const user = await createTestUser({ status: UserStatus.ACTIVE });

    // Cambiar estado directamente en DB (sin pasar por service)
    await query(`
      UPDATE auth_management.profiles
      SET status = 'suspended'
      WHERE id = $1
    `, [user.id]);

    // Verificar que trigger creó audit log
    const auditLogs = await query(`
      SELECT * FROM audit_logging.audit_logs
      WHERE resource_id = $1
        AND resource_type = 'user_status'
      ORDER BY created_at DESC
      LIMIT 1
    `, [user.id]);

    expect(auditLogs.rows).toHaveLength(1);
    expect(auditLogs.rows[0].action).toBe('suspend');
    expect(auditLogs.rows[0].details.old_status).toBe('active');
    expect(auditLogs.rows[0].details.new_status).toBe('suspended');
  });
});

📚 Referencias Adicionales

Documentos Relacionados

Regulaciones y Compliance

Recursos Técnicos


📅 Historial de Cambios

Versión Fecha Autor Cambios
1.0 2025-11-17 Tech Team Creación inicial adaptada de GAMILIT con contexto de construcción

Documento: MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md Ruta absoluta: [RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md Generado: 2025-11-17 Mantenedores: @tech-lead @backend-team @frontend-team