"""Tenancy models (``TENANCY.md`` §2, §6).""" from __future__ import annotations import uuid from django.conf import settings from django.db import models from django.utils import timezone from .managers import AllObjectsManager, TenantManager __all__ = ["Tenant", "TenantMembership", "TenantInvitation", "PlatformStaff"] class Tenant(models.Model): """One customer company. UUID PK so ids are safe to expose and embed.""" class Status(models.TextChoices): TRIALING = "trialing", "Trialing" ACTIVE = "active", "Active" SUSPENDED = "suspended", "Suspended" ARCHIVED = "archived", "Archived" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) slug = models.SlugField(max_length=100, unique=True) name = models.CharField(max_length=200) status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE) locale = models.CharField(max_length=10, default="es") timezone = models.CharField(max_length=64, default="UTC") metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) suspended_at = models.DateTimeField(null=True, blank=True) archived_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "tenancy_tenant" def __str__(self) -> str: return f"{self.name} ({self.slug})" class TenantMembership(models.Model): """A user's membership in a tenant. The only correct user↔tenant link.""" tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name="memberships") user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="tenant_memberships", ) role = models.CharField(max_length=50, default="member") is_owner = models.BooleanField(default=False) is_active = models.BooleanField(default=True) joined_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "tenancy_membership" unique_together = [("tenant", "user")] indexes = [models.Index(fields=["user", "is_active"])] def __str__(self) -> str: return f"{self.user}@{self.tenant.slug}" class TenantInvitation(models.Model): """A pending invite to join a tenant. Tenant-owned and expiring.""" tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name="invitations") email = models.EmailField() role = models.CharField(max_length=50, default="member") token = models.CharField(max_length=64, unique=True) invited_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) expires_at = models.DateTimeField() accepted_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) objects = TenantManager() all_objects = AllObjectsManager() class Meta: db_table = "tenancy_invitation" indexes = [ models.Index(fields=["tenant", "email"]), models.Index(fields=["tenant", "token"]), ] def is_expired(self) -> bool: return self.expires_at < timezone.now() def __str__(self) -> str: return f"invite:{self.email}@{self.tenant.slug}" class PlatformStaff(models.Model): """Marks an InfraSynth team member allowed explicit, audited cross-tenant access. ``User`` is external (``AUTH_USER_MODEL``), so this cannot be a field on it. """ user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="platform_staff", ) reason = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "tenancy_platform_staff" def __str__(self) -> str: return f"platform-staff:{self.user_id}"