"""Identity domain: Organization, User, Team, Project. SaaS tenancy: Organization == tenant boundary └── Teams └── Projects └── Artifacts + Relationships + Jobs """ from __future__ import annotations from typing import Optional, List from pydantic import Field, field_validator from asf_domain.base import ( ASFBaseModel, TenantScopedModel, ASFFieldID, UTCDatetime, generate_id, ) # ====================================================================== # Organization (tenant) # ====================================================================== class OrganizationBase(ASFBaseModel): name: str = Field( ..., min_length=2, max_length=120, description="Human readable organization / tenant name", examples=["Azurian Labs", "Contoso Retail"], ) slug: str = Field( ..., min_length=3, max_length=60, pattern=r"^[a-z0-9][a-z0-9\-]{1,58}[a-z0-9]$", description="URL-safe unique identifier for the tenant", examples=["azurian-labs"], ) plan: str = Field( "free", description="Subscription tier placeholder", examples=["free", "team", "enterprise"], ) is_active: bool = True class OrganizationCreate(OrganizationBase): """Payload used to create a new Organization (tenant).""" pass class OrganizationUpdate(ASFBaseModel): name: Optional[str] = Field(None, min_length=2, max_length=120) slug: Optional[str] = Field(None, min_length=3, max_length=60) plan: Optional[str] = None is_active: Optional[bool] = None class Organization(OrganizationBase): """Stored Organization record — also the tenant boundary record.""" id: ASFFieldID = Field(default_factory=generate_id) created_at: UTCDatetime = Field( ..., description="Creation timestamp (UTC, written on insert)" ) updated_at: UTCDatetime = Field( ..., description="Last update timestamp (UTC, written on every write)" ) metadata: dict = Field( default_factory=dict, description="Tenant-level extensible metadata (billing id, features, …)", ) # ====================================================================== # User (bootstrap skeleton; real auth lives outside domain) # ====================================================================== class User(TenantScopedModel): """Minimal User record used for ownership / audit references.""" id: ASFFieldID = Field(default_factory=generate_id) email: str = Field(..., max_length=255) display_name: str = Field(..., max_length=120) roles: List[str] = Field(default_factory=lambda: ["member"]) is_active: bool = True created_at: UTCDatetime = ... updated_at: UTCDatetime = ... @field_validator("email") @classmethod def _lower_email(cls, v: str) -> str: return v.strip().lower() # ====================================================================== # Team # ====================================================================== class Team(TenantScopedModel): id: ASFFieldID = Field(default_factory=generate_id) name: str = Field(..., min_length=2, max_length=80) description: Optional[str] = Field(None, max_length=500) member_ids: List[ASFFieldID] = Field(default_factory=list) created_at: UTCDatetime = ... updated_at: UTCDatetime = ... # ====================================================================== # Project # ====================================================================== class ProjectBase(ASFBaseModel): name: str = Field(..., min_length=2, max_length=120) key: str = Field( ..., min_length=2, max_length=20, pattern=r"^[A-Z][A-Z0-9]{1,19}$", description="Short project key (e.g. ASF, PROJ) used in human readable refs", examples=["ASF", "PRJ"], ) description: Optional[str] = Field(None, max_length=1000) default_branch: str = Field("main", max_length=100) is_active: bool = True class ProjectCreate(ProjectBase): """Create payload; tenant_id is injected server side from auth context.""" pass class ProjectUpdate(ASFBaseModel): name: Optional[str] = Field(None, min_length=2, max_length=120) key: Optional[str] = Field(None, min_length=2, max_length=20) description: Optional[str] = Field(None, max_length=1000) default_branch: Optional[str] = None is_active: Optional[bool] = None class Project(TenantScopedModel, ProjectBase): """Stored Project record. Lives inside a tenant.""" id: ASFFieldID = Field(default_factory=generate_id) team_ids: List[ASFFieldID] = Field(default_factory=list) created_at: UTCDatetime = ... updated_at: UTCDatetime = ... metadata: dict = Field(default_factory=dict)