# 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](../especificaciones/ET-AUTH-002-estados-cuenta.md) *(Pendiente)*

### Reutilización de Catálogo
♻️ **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:**
```sql
-- 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:**
```sql
-- 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:**
```sql
-- 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`
```typescript
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`
```typescript
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](../implementacion/TRACEABILITY.yml#L45-L78)

---

## 📝 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 |
|------------|-------|
| ❌ `pending` → `suspended` | No tiene sentido suspender cuenta no verificada (simplemente eliminar invitación) |
| ❌ `pending` → `banned` | No tiene sentido banear cuenta no verificada |
| ❌ `inactive` → `suspended` | Usuario ya desactivó voluntariamente, no hay necesidad de suspender |
| ❌ `inactive` → `banned` | Si se requiere baneo, reactivar primero y luego banear desde `active` |
| ❌ `suspended` → `inactive` | 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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 `pending` → `active`
5. Sistema actualiza `user_constructoras.status` de `pending` → `active`
6. Sistema audita cambio en `audit_logs`:
   ```json
   {
     "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:
   ```sql
   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ón** → **Mi 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 `active` → `inactive`
9. Sistema audita cambio:
   ```json
   {
     "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 `inactive` → `active`
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:
   ```sql
   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**

```typescript
// 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:**
```typescript
// 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**

```sql
-- 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**

```typescript
// 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**

```typescript
// 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**

```typescript
// 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...
}
```

```sql
-- 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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
- 📄 [RF-AUTH-001: Sistema de Roles de Construcción](./RF-AUTH-001-roles-construccion.md) - Estados interactúan con roles
- 📄 [RF-AUTH-003: Multi-tenancy por Constructora](./RF-AUTH-003-multi-tenancy.md) *(Pendiente)* - Estados por constructora
- 📄 [US-FUND-001: Autenticación Básica JWT](../historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) - Login valida estado
- 📄 [US-FUND-005: Sistema de Sesiones](../historias-usuario/US-FUND-005-sistema-sesiones.md) *(Pendiente)* - Cerrar sesiones al suspender

### Regulaciones y Compliance
- [GDPR Article 17: Right to Erasure](https://gdpr-info.eu/art-17-gdpr/) - Derecho al olvido
- [GDPR Article 20: Right to Data Portability](https://gdpr-info.eu/art-20-gdpr/) - Exportar datos personales
- [Ley Federal de Protección de Datos Personales (México)](https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf) - Protección de datos en México

### Recursos Técnicos
- [PostgreSQL ENUM Types](https://www.postgresql.org/docs/current/datatype-enum.html)
- [NestJS Guards](https://docs.nestjs.com/guards)
- [NestJS Middleware](https://docs.nestjs.com/middleware)

---

## 📅 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
