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:
- Duplica datos: esos campos serían un espejo de los que ya están en
Cliente. - Permite estados imposibles: con campos mutuamente excluyentes puede existir una fila con
ambos rellenos o con ninguno, y cada consulta necesita un
if. - Obliga a migrar datos: cuando el invitado se registre, habría que trasladar sus datos de
ReservaaCliente.
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:
Clienteresponde "¿quién se hospeda?" — nombres, contacto, historial de estancias. Es negocio hotelero.Usuarioresponde "¿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á en01-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
Clienteen perfil de huésped y credencial a la vez: dos responsabilidades en una clase. - Un campo
esInvitado. Es información redundante: equivale exactamente auser 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.