# models/schema/team.py """ Pydantic schemas for vendor team management. This module defines request/response schemas for: - Team member listing - Team member invitation - Team member updates - Role management """ from datetime import datetime from typing import Optional, List from pydantic import BaseModel, EmailStr, Field, field_validator # ============================================================================ # Role Schemas # ============================================================================ class RoleBase(BaseModel): """Base role schema.""" name: str = Field(..., min_length=1, max_length=100, description="Role name") permissions: List[str] = Field(default_factory=list, description="List of permission strings") class RoleCreate(RoleBase): """Schema for creating a role.""" pass class RoleUpdate(BaseModel): """Schema for updating a role.""" name: Optional[str] = Field(None, min_length=1, max_length=100) permissions: Optional[List[str]] = None class RoleResponse(RoleBase): """Schema for role response.""" id: int vendor_id: int created_at: datetime updated_at: datetime class Config: from_attributes = True # Pydantic v2 (use orm_mode = True for v1) class RoleListResponse(BaseModel): """Schema for role list response.""" roles: List[RoleResponse] total: int # ============================================================================ # Team Member Schemas # ============================================================================ class TeamMemberBase(BaseModel): """Base team member schema.""" email: EmailStr = Field(..., description="Team member email address") first_name: Optional[str] = Field(None, max_length=100) last_name: Optional[str] = Field(None, max_length=100) class TeamMemberInvite(TeamMemberBase): """Schema for inviting a team member.""" role_id: Optional[int] = Field(None, description="Role ID to assign (for preset roles)") role_name: Optional[str] = Field(None, description="Role name (manager, staff, support, etc.)") custom_permissions: Optional[List[str]] = Field( None, description="Custom permissions (overrides role preset)" ) @field_validator('role_name') def validate_role_name(cls, v): """Validate role name is in allowed presets.""" if v is not None: allowed_roles = ['manager', 'staff', 'support', 'viewer', 'marketing'] if v.lower() not in allowed_roles: raise ValueError( f"Role name must be one of: {', '.join(allowed_roles)}" ) return v.lower() if v else v @field_validator('custom_permissions') def validate_custom_permissions(cls, v, values): """Ensure either role_id/role_name OR custom_permissions is provided.""" if v is not None and len(v) > 0: # If custom permissions provided, role_name should be provided too if 'role_name' not in values or not values['role_name']: raise ValueError( "role_name is required when providing custom_permissions" ) return v class TeamMemberUpdate(BaseModel): """Schema for updating a team member.""" role_id: Optional[int] = Field(None, description="New role ID") is_active: Optional[bool] = Field(None, description="Active status") class TeamMemberResponse(BaseModel): """Schema for team member response.""" id: int = Field(..., description="User ID") email: EmailStr username: str first_name: Optional[str] last_name: Optional[str] full_name: str user_type: str = Field(..., description="'owner' or 'member'") role_name: str = Field(..., description="Role name") role_id: Optional[int] permissions: List[str] = Field(default_factory=list, description="User's permissions") is_active: bool is_owner: bool invitation_pending: bool = Field( default=False, description="True if invitation not yet accepted" ) invited_at: Optional[datetime] = Field(None, description="When invitation was sent") accepted_at: Optional[datetime] = Field(None, description="When invitation was accepted") joined_at: datetime = Field(..., description="When user joined vendor") class Config: from_attributes = True class TeamMemberListResponse(BaseModel): """Schema for team member list response.""" members: List[TeamMemberResponse] total: int active_count: int pending_invitations: int # ============================================================================ # Invitation Schemas # ============================================================================ class InvitationAccept(BaseModel): """Schema for accepting a team invitation.""" invitation_token: str = Field(..., min_length=32, description="Invitation token from email") password: str = Field( ..., min_length=8, max_length=128, description="Password for new account" ) first_name: str = Field(..., min_length=1, max_length=100) last_name: str = Field(..., min_length=1, max_length=100) @field_validator('password') def validate_password_strength(cls, v): """Validate password meets minimum requirements.""" if len(v) < 8: raise ValueError("Password must be at least 8 characters long") has_upper = any(c.isupper() for c in v) has_lower = any(c.islower() for c in v) has_digit = any(c.isdigit() for c in v) if not (has_upper and has_lower and has_digit): raise ValueError( "Password must contain at least one uppercase letter, " "one lowercase letter, and one digit" ) return v class InvitationResponse(BaseModel): """Schema for invitation response.""" message: str email: EmailStr role: str invitation_token: Optional[str] = Field( None, description="Token (only returned in dev/test environments)" ) invitation_sent: bool = Field(default=True) class InvitationAcceptResponse(BaseModel): """Schema for invitation acceptance response.""" message: str vendor: dict = Field(..., description="Vendor information") user: dict = Field(..., description="User information") role: str # ============================================================================ # Team Statistics Schema # ============================================================================ class TeamStatistics(BaseModel): """Schema for team statistics.""" total_members: int active_members: int inactive_members: int pending_invitations: int owners: int team_members: int roles_breakdown: dict = Field( default_factory=dict, description="Count of members per role" ) # ============================================================================ # Bulk Operations Schemas # ============================================================================ class BulkRemoveRequest(BaseModel): """Schema for bulk removing team members.""" user_ids: List[int] = Field(..., min_items=1, description="List of user IDs to remove") class BulkRemoveResponse(BaseModel): """Schema for bulk remove response.""" success_count: int failed_count: int errors: List[dict] = Field(default_factory=list) # ============================================================================ # Permission Check Schemas # ============================================================================ class PermissionCheckRequest(BaseModel): """Schema for checking permissions.""" permissions: List[str] = Field(..., min_items=1, description="Permissions to check") class PermissionCheckResponse(BaseModel): """Schema for permission check response.""" has_all: bool = Field(..., description="True if user has all permissions") has_any: bool = Field(..., description="True if user has any permission") granted: List[str] = Field(default_factory=list, description="Permissions user has") denied: List[str] = Field(default_factory=list, description="Permissions user lacks") class UserPermissionsResponse(BaseModel): """Schema for user's permissions response.""" permissions: List[str] = Field(default_factory=list) permission_count: int is_owner: bool role_name: Optional[str] = None # ============================================================================ # Error Response Schema # ============================================================================ class TeamErrorResponse(BaseModel): """Schema for team operation errors.""" error_code: str message: str details: Optional[dict] = None