CreaRack-SL

Modelo InvitationCode — código invite-only para registro F1

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

CampoTipoNotas
codeCharField (unique, max_length=64)Código uppercase normalizado en save(). Ej: FOUNDER-ACME
planFK(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_usesPositiveIntegerField (default=1)Número máximo de usos permitidos
uses_countPositiveIntegerField (default=0)Contador de altas realizadas con este código
expires_atDateTimeField (nullable)Caducidad. NULL = sin expiración
is_activeBooleanField (default=True)Flag de desactivación manual
notesTextField (blank=True)Notas internas para administradores (quién lo creó, para quién, etc.)
created_byFK(User, SET_NULL, related_name=“invitation_codes_created”)Admin que creó el código (trazabilidad)
created_atDateTimeField (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):

  1. Código existe y activo (is_active == True)
  2. No está expirado (not is_expired)
  3. Tiene usos disponibles (has_uses_left)
  4. 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_at
  • list_filter: is_active, plan
  • search_fields: code, notes
  • readonly_fields: uses_count, created_at
  • autocomplete_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_count es 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]]