Identidad y autenticación - MiguelAngelMarinMejia1/ArquitecturaDeSoftware-Proyecto GitHub Wiki

El problema

Hotel MDJ debe permitir reservar sin cuenta —así funciona la gran mayoría de reservas hoteleras reales— pero también debe estar preparado para el login, el historial de reservas y los puntos de fidelidad que llegarán en una etapa posterior.

La solución ingenua sería añadir a Reserva un conjunto de campos de invitado anulables (nombre_invitado, email_invitado, …) más una referencia opcional a la cuenta. Se descartó por tres razones:

  1. Duplica datos: esos campos serían un espejo de los que ya están en Cliente.
  2. Permite estados imposibles: con campos mutuamente excluyentes puede existir una fila con ambos rellenos o con ninguno, y cada consulta necesita un if.
  3. Obliga a migrar datos: cuando el invitado se registre, habría que trasladar sus datos de Reserva a Cliente.

La solución: el perfil de huésped siempre existe

Se adoptó el modelo de los sistemas de gestión hotelera reales (Oracle OPERA, Cloudbeds, Mews): el perfil de huésped es una entidad de primera clase que nace en el momento de reservar, tenga o no cuenta. Lo opcional es la cuenta, no el perfil.

┌─────────────────────────┐                    ┌──────────────────────────────┐
│        Cliente          │                    │   Usuario   «infraestructura»│
│       (dominio)         │  0..1        0..1  │   django.contrib.auth.User   │
│                         │────────────────────│                              │
│ nombres, apellidos      │      "tiene        │ username, password (hash)    │
│ email (único)           │       cuenta"      │ last_login, is_active        │
│ telefono, pais          │                    │ permisos, grupos             │
│ documentoIdentidad      │                    │                              │
└───────────┬─────────────┘                    └──────────────────────────────┘
            │ 1
            │ realiza
            ▼ 0..*
┌─────────────────────────┐
│         Reserva         │
└─────────────────────────┘

Se lee así: un Cliente puede tener cuenta, y una cuenta puede corresponder a un Cliente. La relación Reserva → Cliente es obligatoria y no cambia nunca.

Por qué son dos clases y no una

Cliente y Usuario responden a preguntas distintas:

  • Cliente responde "¿quién se hospeda?" — nombres, contacto, historial de estancias. Es negocio hotelero.
  • Usuario responde "¿quién puede entrar al sistema?" — credenciales, sesión, permisos. Es infraestructura de identidad.

Cambian por razones distintas: añadir login con Google modifica Usuario y Cliente ni se entera. Y la vida real produce constantemente casos donde solo existe uno de los dos:

Situación Cliente Usuario
Invitado reserva por la web Sí No
Huésped registrado Sí Sí
Recepcionista del hotel No Sí
Administrador del sistema No Sí

Fusionarlos en una sola clase obligaría a que cada recepcionista tuviera un perfil de huésped falso.

Por qué el diagrama de clases original era correcto

El diagrama entregado contiene solo clases de negocio, y eso está bien: la autenticación no pertenece al modelo de dominio. Lo único que se le añadió es la clase Usuario, marcada con el estereotipo «infraestructura» para dejar visible que no forma parte del negocio y que la implementa el framework, no el equipo.

Diagrama actualizado

classDiagram
    class Cliente {
        -idCliente: int
        -nombres: string
        -apellidos: string
        -email: string
        -telefono: string
        -documentoIdentidad: string
        -pais: string
        +consultarHistorialReservas() List~Reserva~
    }

    class Usuario {
        <<infraestructura>>
        -username: string
        -passwordHash: string
        -lastLogin: Date
        -isActive: bool
    }

    class Reserva {
        -idReserva: int
        -fechaEntrada: Date
        -fechaSalida: Date
        -fechaCreacion: Date
        -numAdultos: int
        -numNinos: int
        -codigoConfirmacion: string
        -estado: string
        -subtotal: float
        -impuestos: float
        -total: float
    }

    class Habitacion {
        -numero: string
        -piso: int
        -estado: string
        -activa: bool
    }

    class TipoHabitacion {
        -nombre: string
        -descripcion: string
        -capacidadMaxima: int
        -tipoCama: string
        -precioNocheBase: float
    }

    class Pago {
        -idPago: int
        -monto: float
        -fecha: Date
        -metodoPago: string
        -estado: string
        -referenciaPasarela: string
    }

    class Factura {
        -idFactura: int
        -fechaEmision: Date
        -subtotal: float
        -impuestos: float
        -total: float
    }

    Cliente "0..1" -- "0..1" Usuario : tiene cuenta
    Cliente "1" --> "0..*" Reserva : realiza
    Reserva "1" --> "1..*" Habitacion : reserva
    Habitacion "0..*" --> "1" TipoHabitacion : es de tipo
    Reserva "1" --> "1" Pago : genera
    Pago "1" --> "1" Factura : produce

Las operaciones de negocio del diagrama original (calcularTotal, verificarDisponibilidad, procesarPago…) no aparecen dentro de las clases porque se implementaron en la capa de dominio y de servicios. El mapeo completo está en 01-arquitectura-y-estructura.md.

Implementación

class Customer(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)          # clave natural de deduplicación
    phone = models.CharField(max_length=30)
    country = models.CharField(max_length=60, blank=True)
    document_id = models.CharField(max_length=30, null=True, blank=True)
    marketing_consent = models.BooleanField(default=False)

    user = models.OneToOneField(                    # el puente con la autenticación
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        related_name="customer_profile",
    )

Qué NO se guarda en Cliente

  • Contraseña. Django ya implementa hashing PBKDF2, validadores, recuperación, sesiones y protección contra ataques de temporización. Reimplementar autenticación en una tabla propia es un riesgo de seguridad innecesario, y convertiría a Cliente en perfil de huésped y credencial a la vez: dos responsabilidades en una clase.
  • Un campo esInvitado. Es información redundante: equivale exactamente a user is None. Un booleano que duplica un dato existente puede desincronizarse y producir filas marcadas como invitado con una cuenta asignada.

Deduplicación por correo

customer, created = Customer.objects.get_or_create(
    email=email,
    defaults={...},
)

Un huésped que vuelve a reservar reutiliza su perfil en lugar de crear un duplicado. Los datos de contacto se refrescan en cada reserva porque la más reciente trae el teléfono más actual. El consentimiento comercial solo se concede, nunca se revoca silenciosamente por una casilla sin marcar.

La ruta hacia el login (Entrega 2)

El gancho ya está implementado:

def link_to_account(self, customer: Customer, user) -> Customer:
    customer.user = user
    customer.save(update_fields=["user"])
    return customer

Cuando alguien que reservó como invitado se registre con el mismo correo, se enlaza su perfil existente a la cuenta nueva y hereda automáticamente todo su historial de reservas —porque las reservas siempre apuntaron al perfil, no a la cuenta. No hay datos que migrar.

Sobre esa base, los puntos de fidelidad se acumulan en el perfil, no en la cuenta: un huésped puede haber acumulado estancias antes de registrarse y no debería perderlas.

Alcance de esta entrega

Se implementó únicamente el gancho en el modelo. No hay pantallas de registro ni de inicio de sesión, ni prellenado de formularios para usuarios autenticados. Reservar no requiere cuenta, y la consulta posterior de una reserva se resuelve con código de confirmación más correo electrónico, como se explica en 06-seguridad-datos-personales.md.