"""Pydantic schemas for admin panel endpoints.""" from datetime import datetime from typing import Literal, Optional from uuid import UUID from pydantic import BaseModel, EmailStr, Field # --- Dashboard --- class DashboardMetrics(BaseModel): total_users: int active_subscriptions: int paid_accounts: int total_trees: int class ActivityEntry(BaseModel): id: UUID user_email: Optional[str] = None action: str resource_type: str resource_id: Optional[UUID] = None details: Optional[dict] = None ip_address: Optional[str] = None created_at: datetime class Config: from_attributes = True # --- Admin Accounts & People Search --- class AdminUserListItem(BaseModel): id: UUID email: EmailStr name: str role: str is_super_admin: bool = False is_active: bool = True account_id: Optional[UUID] = None account_role: Optional[str] = None account_name: Optional[str] = None account_display_code: Optional[str] = None created_at: datetime last_login: Optional[datetime] = None deleted_at: Optional[datetime] = None class AdminUserListResponse(BaseModel): items: list[AdminUserListItem] total: int page: int per_page: int class AdminAccountMember(BaseModel): id: UUID email: EmailStr name: str role: str is_super_admin: bool = False is_active: bool = True account_role: Optional[str] = None created_at: datetime last_login: Optional[datetime] = None deleted_at: Optional[datetime] = None class AdminAccountOwnerSummary(BaseModel): id: UUID name: str email: EmailStr class AdminAccountSubscriptionSummary(BaseModel): id: UUID plan: str status: str billing_interval: Optional[str] = None current_period_end: Optional[datetime] = None cancel_at_period_end: bool = False class AdminAccountUsageSummary(BaseModel): tree_count: int = 0 session_count_this_month: int = 0 class AdminAccountInviteSummary(BaseModel): id: UUID email: EmailStr role: str expires_at: Optional[datetime] = None created_at: datetime used_at: Optional[datetime] = None class AdminAccountListItem(BaseModel): id: UUID name: str display_code: str created_at: datetime owner_id: Optional[UUID] = None owner: Optional[AdminAccountOwnerSummary] = None subscription: Optional[AdminAccountSubscriptionSummary] = None usage: AdminAccountUsageSummary = Field(default_factory=AdminAccountUsageSummary) member_count: int = 0 active_member_count: int = 0 pending_invite_count: int = 0 sso_enabled: bool = False branding_company_name: Optional[str] = None members: list[AdminAccountMember] = Field(default_factory=list) class AdminAccountListResponse(BaseModel): items: list[AdminAccountListItem] total: int page: int per_page: int class AdminAccountDetailResponse(AdminAccountListItem): invites: list[AdminAccountInviteSummary] = Field(default_factory=list) class AdminAccountCreate(BaseModel): name: str = Field(..., min_length=1, max_length=255) plan: Literal["free", "pro", "team"] = "free" owner_email: Optional[EmailStr] = Field(None, description="Email of an existing user to set as owner") class AdminAccountUpdate(BaseModel): name: str = Field(..., min_length=1, max_length=255) # --- Audit Logs --- class AuditLogEntry(BaseModel): id: UUID user_id: UUID user_email: Optional[str] = None action: str resource_type: str resource_id: Optional[UUID] = None details: Optional[dict] = None ip_address: Optional[str] = None created_at: datetime class Config: from_attributes = True class AuditLogListResponse(BaseModel): items: list[AuditLogEntry] total: int page: int per_page: int # --- Plan Limits --- class PlanLimitResponse(BaseModel): plan: str max_trees: Optional[int] = None max_sessions_per_month: Optional[int] = None max_users: Optional[int] = None custom_branding: bool = False priority_support: bool = False export_formats: list = [] class Config: from_attributes = True class PlanLimitWithBillingResponse(PlanLimitResponse): """PlanLimits + plan_billing fields merged. Billing fields are None when no plan_billing row exists for the plan yet.""" display_name: Optional[str] = None description: Optional[str] = None monthly_price_cents: Optional[int] = None annual_price_cents: Optional[int] = None stripe_product_id: Optional[str] = None stripe_monthly_price_id: Optional[str] = None stripe_annual_price_id: Optional[str] = None is_public: Optional[bool] = None is_archived: Optional[bool] = None sort_order: Optional[int] = None class PlanLimitUpdate(BaseModel): plan: str max_trees: Optional[int] = None max_sessions_per_month: Optional[int] = None max_users: Optional[int] = None custom_branding: bool = False priority_support: bool = False export_formats: list = Field(default_factory=lambda: ["markdown", "text"]) # plan_billing fields — all optional, partial-update semantics. If any are # set in the body, the admin endpoint upserts the plan_billing row in the # same transaction. display_name: Optional[str] = None description: Optional[str] = None monthly_price_cents: Optional[int] = None annual_price_cents: Optional[int] = None stripe_product_id: Optional[str] = None stripe_monthly_price_id: Optional[str] = None stripe_annual_price_id: Optional[str] = None is_public: Optional[bool] = None is_archived: Optional[bool] = None sort_order: Optional[int] = None class AccountOverrideCreate(BaseModel): account_display_code: str = Field(..., description="Account display code to look up") override_max_trees: Optional[int] = None override_max_sessions_per_month: Optional[int] = None override_max_users: Optional[int] = None note: Optional[str] = None class AccountOverrideUpdate(BaseModel): override_max_trees: Optional[int] = None override_max_sessions_per_month: Optional[int] = None override_max_users: Optional[int] = None note: Optional[str] = None class AccountOverrideResponse(BaseModel): id: UUID account_id: UUID account_name: Optional[str] = None account_display_code: Optional[str] = None override_max_trees: Optional[int] = None override_max_sessions_per_month: Optional[int] = None override_max_users: Optional[int] = None note: Optional[str] = None created_at: datetime updated_at: datetime class Config: from_attributes = True # --- Feature Flags --- class FeatureFlagCreate(BaseModel): flag_key: str = Field(..., max_length=100) display_name: str = Field(..., max_length=255) description: Optional[str] = None class FeatureFlagUpdate(BaseModel): display_name: Optional[str] = None description: Optional[str] = None class PlanDefaultEntry(BaseModel): plan: str enabled: bool class FeatureFlagResponse(BaseModel): id: UUID flag_key: str display_name: str description: Optional[str] = None plan_defaults: list[PlanDefaultEntry] = [] created_at: datetime class Config: from_attributes = True class PlanDefaultUpdate(BaseModel): plan: str flag_id: UUID enabled: bool class AccountFeatureOverrideCreate(BaseModel): account_display_code: str flag_id: UUID enabled: bool note: Optional[str] = None class AccountFeatureOverrideResponse(BaseModel): id: UUID account_id: UUID account_display_code: Optional[str] = None flag_id: UUID flag_key: Optional[str] = None flag_display_name: Optional[str] = None enabled: bool note: Optional[str] = None created_at: datetime class Config: from_attributes = True # --- Platform Settings --- class SettingsResponse(BaseModel): settings: dict class SettingsUpdate(BaseModel): settings: dict = Field(..., description="Key-value pairs to update") # --- Global Categories --- class GlobalCategoryCreate(BaseModel): name: str = Field(..., min_length=1, max_length=100) slug: str = Field(..., min_length=1, max_length=100) description: Optional[str] = None class GlobalCategoryUpdate(BaseModel): name: Optional[str] = Field(None, max_length=100) slug: Optional[str] = Field(None, max_length=100) description: Optional[str] = None class GlobalCategoryResponse(BaseModel): id: UUID name: str slug: str description: Optional[str] = None account_id: Optional[UUID] = None tree_count: int = 0 class Config: from_attributes = True # --- Move User --- class MoveUserAccount(BaseModel): display_code: str = Field(..., description="Target account display code") # --- Admin User Creation --- class AdminUserCreate(BaseModel): email: EmailStr name: str = Field(..., min_length=1, max_length=255) account_mode: Literal["existing", "personal"] account_display_code: Optional[str] = Field(None, description="Required when account_mode='existing'") account_role: Optional[Literal["owner", "admin", "engineer", "viewer"]] = Field(None, description="Required when account_mode='existing'") send_email: bool = True class AdminUserCreateResponse(BaseModel): user: dict # UserResponse fields temporary_password: str email_sent: bool # --- Admin Password Reset --- class AdminPasswordReset(BaseModel): mode: Literal["email_link", "temp_password"] class AdminPasswordResetResponse(BaseModel): message: str temporary_password: Optional[str] = None email_sent: bool = False # --- Hard Delete Precheck --- class HardDeleteCheckResponse(BaseModel): can_delete: bool blockers: dict