""" Smooth Schedule Core Models Multi-tenancy, Domain management, and Permission Grant system """ from django.db import models from django.utils import timezone from django_tenants.models import TenantMixin, DomainMixin from datetime import timedelta class Tenant(TenantMixin): """ Tenant model for multi-schema architecture. Each tenant gets their own PostgreSQL schema for complete data isolation. """ name = models.CharField(max_length=100) created_on = models.DateField(auto_now_add=True) # Subscription & billing is_active = models.BooleanField(default=True) subscription_tier = models.CharField( max_length=50, choices=[ ('FREE', 'Free Trial'), ('STARTER', 'Starter'), ('PROFESSIONAL', 'Professional'), ('ENTERPRISE', 'Enterprise'), ], default='FREE' ) # Feature flags max_users = models.IntegerField(default=5) max_resources = models.IntegerField(default=10) # Metadata contact_email = models.EmailField(blank=True) phone = models.CharField(max_length=20, blank=True) # Auto-created fields from TenantMixin: # - schema_name (unique, indexed) # - auto_create_schema # - auto_drop_schema class Meta: ordering = ['name'] def __str__(self): return self.name class Domain(DomainMixin): """ Domain model for tenant routing. Supports both subdomain (tenant.chronoflow.com) and custom domains. """ # Inherited from DomainMixin: # - domain (unique, primary key) # - tenant (ForeignKey to Tenant) # - is_primary (boolean) # Route53 integration fields for custom domains is_custom_domain = models.BooleanField( default=False, help_text="True if this is a custom domain (not a subdomain of smoothschedule.com)" ) route53_zone_id = models.CharField( max_length=100, blank=True, null=True, help_text="AWS Route53 Hosted Zone ID for this custom domain" ) route53_record_set_id = models.CharField( max_length=100, blank=True, null=True, help_text="Route53 Record Set ID for DNS verification" ) # SSL certificate management (for future AWS ACM integration) ssl_certificate_arn = models.CharField( max_length=200, blank=True, null=True, help_text="AWS ACM Certificate ARN for HTTPS" ) verified_at = models.DateTimeField( null=True, blank=True, help_text="When the custom domain was verified via DNS" ) class Meta: ordering = ['domain'] def __str__(self): domain_type = "Custom" if self.is_custom_domain else "Subdomain" return f"{self.domain} ({domain_type})" def is_verified(self): """Check if custom domain is verified""" if not self.is_custom_domain: return True # Subdomains are always verified return self.verified_at is not None class PermissionGrant(models.Model): """ Time-limited permission grants (30-minute window). Used for temporary elevated access without permanently changing user permissions. Example use cases: - Support agent needs temporary access to tenant data - Sales demo requiring elevated permissions - Cross-tenant operations during migrations """ grantor = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='granted_permissions', help_text="User who granted the permission" ) grantee = models.ForeignKey( 'users.User', on_delete=models.CASCADE, related_name='received_permissions', help_text="User who received the permission" ) action = models.CharField( max_length=100, help_text="Specific action or permission being granted (e.g., 'view_billing', 'edit_settings')" ) reason = models.TextField( blank=True, help_text="Justification for granting this permission (audit trail)" ) granted_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField( help_text="When this permission grant expires" ) revoked_at = models.DateTimeField( null=True, blank=True, help_text="If manually revoked before expiration" ) # Audit metadata ip_address = models.GenericIPAddressField( null=True, blank=True, help_text="IP address where grant was created" ) user_agent = models.TextField( blank=True, help_text="Browser/client user agent" ) class Meta: ordering = ['-granted_at'] indexes = [ models.Index(fields=['grantee', 'expires_at']), models.Index(fields=['grantor', 'granted_at']), ] def __str__(self): return f"{self.grantor} → {self.grantee}: {self.action} (expires {self.expires_at})" def is_active(self): """ Check if this permission grant is currently active. Returns False if expired or revoked. """ now = timezone.now() # Check if revoked if self.revoked_at is not None: return False # Check if expired if now >= self.expires_at: return False return True def revoke(self): """Manually revoke this permission grant""" if self.revoked_at is None: self.revoked_at = timezone.now() self.save(update_fields=['revoked_at']) def time_remaining(self): """Get timedelta of remaining active time (or None if expired/revoked)""" if not self.is_active(): return None now = timezone.now() return self.expires_at - now @classmethod def create_grant(cls, grantor, grantee, action, reason="", duration_minutes=30, ip_address=None, user_agent=""): """ Factory method to create a time-limited permission grant. Args: grantor: User granting the permission grantee: User receiving the permission action: Permission action string reason: Justification for audit trail duration_minutes: How long the grant is valid (default 30) ip_address: IP address of the request user_agent: Browser user agent Returns: PermissionGrant instance """ now = timezone.now() expires_at = now + timedelta(minutes=duration_minutes) return cls.objects.create( grantor=grantor, grantee=grantee, action=action, reason=reason, expires_at=expires_at, ip_address=ip_address, user_agent=user_agent ) class TierLimit(models.Model): """ Defines resource limits for each subscription tier. Used by HasQuota permission to enforce hard blocks. """ tier = models.CharField( max_length=50, choices=[ ('FREE', 'Free Trial'), ('STARTER', 'Starter'), ('PROFESSIONAL', 'Professional'), ('ENTERPRISE', 'Enterprise'), ] ) feature_code = models.CharField( max_length=100, help_text="Feature code (e.g., 'MAX_RESOURCES', 'MAX_USERS')" ) limit = models.IntegerField( default=0, help_text="Maximum allowed count for this feature" ) class Meta: unique_together = ['tier', 'feature_code'] ordering = ['tier', 'feature_code'] def __str__(self): return f"{self.tier} - {self.feature_code}: {self.limit}"