Descripción
Modelo global (sin FK a Organization) que almacena códigos de invitación para el registro invite-only (/signup, PR-A #137 de F1).
Cada código otorga su plan a la organización que nace con él. La tabla vive fuera de las políticas RLS porque es consultada antes de que exista tenant alguno (el registro es público y anónimo).
Campos
| Campo | Tipo | Notas |
|---|---|---|
code | CharField (unique, max_length=64) | Código uppercase normalizado en save(). Ej: FOUNDER-ACME |
plan | FK(Plan, PROTECT, related_name=“invitation_codes”) | El plan que otorga este código a la org nacida. Borrar un Plan que tenga códigos falla ruidosamente (by design). |
max_uses | PositiveIntegerField (default=1) | Número máximo de usos permitidos |
uses_count | PositiveIntegerField (default=0) | Contador de altas realizadas con este código |
expires_at | DateTimeField (nullable) | Caducidad. NULL = sin expiración |
is_active | BooleanField (default=True) | Flag de desactivación manual |
notes | TextField (blank=True) | Notas internas para administradores (quién lo creó, para quién, etc.) |
created_by | FK(User, SET_NULL, related_name=“invitation_codes_created”) | Admin que creó el código (trazabilidad) |
created_at | DateTimeField (auto_now_add=True) | Timestamp de creación |
Propiedades (computed)
is_expired→self.expires_at is not None and self.expires_at <= timezone.now()has_uses_left→self.uses_count < self.max_uses
Gestión y ciclo de vida
Creación: Actualmente via Django admin (InvitationCodeAdmin en core/admin.py). En PR-B (Settings UI) habrá una interfaz cómoda para superusers.
Validación en el alta (servicio create_signup):
- Código existe y activo (
is_active == True) - No está expirado (
not is_expired) - Tiene usos disponibles (
has_uses_left) - Selección tardía con
select_for_update()para evitar race conditions en el último uso
Consumo atómico: El servicio incremente uses_count dentro de la misma transacción que crea la org. Si hay carrera por el último uso, solo uno gana; el perdedor revierte entera.
Admin mínimo
Clase InvitationCodeAdmin (core/admin.py):
list_display: code, plan, uses_count/max_uses, expires_at, is_active, created_atlist_filter: is_active, plansearch_fields: code, notesreadonly_fields: uses_count, created_atautocomplete_fields: created_by
No hay validación en el admin (ej. fecha de expiración en el pasado); el servidor cubre eso en la vista.
Relación con Organization
Una vez que un código se consume, la org creada almacena la FK a InvitationCode:
Organization.invitation_code = models.ForeignKey(
InvitationCode,
on_delete=models.SET_NULL, # Si se borra el código, org queda con NULL
null=True, blank=True
)
Esta relación es de trazabilidad pura — permite saber qué código usó cada org, pero no es parte de la lógica de alta (el servicio no vuelve a consultar el código una vez que la org está creada).
Notas técnicas
- Tabla global sin RLS: El registro anónimo debe poder consultar sin contexto de tenant.
- PROTECT en FK plan: Obligado para auditoría — no se puede eliminar un plan que haya servido de base para orgs.
- Normalización en
save():self.code.strip().upper()— admite inputs con espacios o minúsculas. - Sin field
used_at: No necesario;uses_countes suficiente. El historial detallado iría en SystemLog (si llegara a enumerarse) o en un audit log aparte en PR-B.
Véase también
- [[entity—core—service—signup]]
- [[entity—core—endpoint—signup]]
- [[feature—core—registro-invite-only-plan-f1]]
- [[entity—core—model—plan]]
- [[entity—core—model—organization]]