"""Pydantic models for database entities.""" from datetime import datetime, date, timezone from enum import Enum from typing import Optional, Dict, Any, List from uuid import UUID from pydantic import BaseModel, Field, ConfigDict class DBModel(BaseModel): """Base model for database entities with extra fields ignored.""" model_config = ConfigDict(extra='ignore') # ==================== ENUMS ==================== class AccountType(str, Enum): """User account type.""" GHOSTWRITER = "ghostwriter" COMPANY = "company" EMPLOYEE = "employee" class OnboardingStatus(str, Enum): """Onboarding status for users.""" PENDING = "pending" PROFILE_SETUP = "profile_setup" POSTS_SCRAPED = "posts_scraped" CATEGORIZING = "categorizing" COMPLETED = "completed" class AuthMethod(str, Enum): """Authentication method for users.""" LINKEDIN_OAUTH = "linkedin_oauth" EMAIL_PASSWORD = "email_password" class InvitationStatus(str, Enum): """Status of an invitation.""" PENDING = "pending" ACCEPTED = "accepted" EXPIRED = "expired" CANCELLED = "cancelled" # ==================== USER & COMPANY MODELS ==================== class Profile(DBModel): """Profile model - extends auth.users with app-specific data. The id is the same as auth.users.id (Supabase handles authentication). """ id: Optional[UUID] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None # Account Type account_type: AccountType = AccountType.GHOSTWRITER # Display name display_name: Optional[str] = None # Onboarding onboarding_status: OnboardingStatus = OnboardingStatus.PENDING onboarding_data: Dict[str, Any] = Field(default_factory=dict) # Links company_id: Optional[UUID] = None # Fields migrated from customers table linkedin_url: Optional[str] = None writing_style_notes: Optional[str] = None metadata: Dict[str, Any] = Field(default_factory=dict) profile_picture: Optional[str] = None creator_email: Optional[str] = None customer_email: Optional[str] = None is_active: bool = True class LinkedInAccount(DBModel): """LinkedIn account connection for auto-posting.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None updated_at: Optional[datetime] = None # LinkedIn Identity linkedin_user_id: str linkedin_vanity_name: Optional[str] = None linkedin_name: Optional[str] = None linkedin_picture: Optional[str] = None # OAuth Tokens (encrypted) access_token: str refresh_token: Optional[str] = None token_expires_at: datetime granted_scopes: List[str] = Field(default_factory=list) # Status is_active: bool = True last_used_at: Optional[datetime] = None last_error: Optional[str] = None last_error_at: Optional[datetime] = None class TelegramAccount(DBModel): """Telegram account connection for bot access.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None updated_at: Optional[datetime] = None telegram_user_id: str telegram_username: Optional[str] = None telegram_first_name: Optional[str] = None telegram_chat_id: str is_active: bool = True last_used_at: Optional[datetime] = None last_error: Optional[str] = None last_error_at: Optional[datetime] = None class User(DBModel): """User model - combines auth.users data with profile data. This is a view model that combines data from Supabase auth.users and our profiles table. Used for compatibility with existing code. """ id: Optional[UUID] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None # From auth.users email: str = "" password_hash: Optional[str] = None # Not stored, Supabase handles it auth_method: AuthMethod = AuthMethod.LINKEDIN_OAUTH # LinkedIn OAuth Data (from auth.users.raw_user_meta_data) linkedin_sub: Optional[str] = None linkedin_vanity_name: Optional[str] = None linkedin_name: Optional[str] = None linkedin_picture: Optional[str] = None # From profiles table account_type: AccountType = AccountType.GHOSTWRITER display_name: Optional[str] = None onboarding_status: OnboardingStatus = OnboardingStatus.PENDING onboarding_data: Dict[str, Any] = Field(default_factory=dict) company_id: Optional[UUID] = None # Fields migrated from customers table linkedin_url: Optional[str] = None writing_style_notes: Optional[str] = None metadata: Dict[str, Any] = Field(default_factory=dict) profile_picture: Optional[str] = None creator_email: Optional[str] = None customer_email: Optional[str] = None is_active: bool = True # Email verification (from auth.users) email_verified: bool = False email_verification_token: Optional[str] = None email_verification_expires_at: Optional[datetime] = None class Company(DBModel): """Company model for company accounts.""" id: Optional[UUID] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None # Company Data name: str description: Optional[str] = None website: Optional[str] = None industry: Optional[str] = None # Strategy company_strategy: Dict[str, Any] = Field(default_factory=dict) owner_user_id: UUID onboarding_completed: bool = False # License key reference (limits are stored in license_keys table) license_key_id: Optional[UUID] = None class Invitation(DBModel): """Invitation model for employee invitations.""" id: Optional[UUID] = None created_at: Optional[datetime] = None email: str token: str expires_at: datetime company_id: UUID invited_by_user_id: UUID status: InvitationStatus = InvitationStatus.PENDING accepted_at: Optional[datetime] = None accepted_by_user_id: Optional[UUID] = None class ExamplePost(DBModel): """Example post model for manual posts during onboarding.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None post_text: str source: str = "manual" # 'manual' | 'reference_profile' source_linkedin_url: Optional[str] = None post_type_id: Optional[UUID] = None class ReferenceProfile(DBModel): """Reference profile for scraping alternative LinkedIn profiles.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None linkedin_url: str name: Optional[str] = None posts_scraped: int = 0 # ==================== CONTENT MODELS ==================== class PostType(DBModel): """Post type model for categorizing different types of posts.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None updated_at: Optional[datetime] = None name: str description: Optional[str] = None identifying_hashtags: List[str] = Field(default_factory=list) identifying_keywords: List[str] = Field(default_factory=list) semantic_properties: Dict[str, Any] = Field(default_factory=dict) analysis: Optional[Dict[str, Any]] = None analysis_generated_at: Optional[datetime] = None analyzed_post_count: int = 0 is_active: bool = True strategy_weight: float = 0.5 class LinkedInProfile(DBModel): """LinkedIn profile model.""" id: Optional[UUID] = None user_id: UUID scraped_at: Optional[datetime] = None profile_data: Dict[str, Any] name: Optional[str] = None headline: Optional[str] = None summary: Optional[str] = None location: Optional[str] = None industry: Optional[str] = None class LinkedInPost(DBModel): """LinkedIn post model.""" id: Optional[UUID] = None user_id: UUID scraped_at: Optional[datetime] = None post_url: Optional[str] = None post_text: str post_date: Optional[datetime] = None likes: int = 0 comments: int = 0 shares: int = 0 raw_data: Optional[Dict[str, Any]] = None # Post type classification fields post_type_id: Optional[UUID] = None classification_method: Optional[str] = None # 'hashtag', 'keyword', 'semantic' classification_confidence: Optional[float] = None class Topic(DBModel): """Topic model.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None title: str description: Optional[str] = None category: Optional[str] = None extracted_from_post_id: Optional[UUID] = None extraction_confidence: Optional[float] = None is_used: bool = False used_at: Optional[datetime] = None target_post_type_id: Optional[UUID] = None class ProfileAnalysis(DBModel): """Profile analysis model.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None writing_style: Dict[str, Any] tone_analysis: Dict[str, Any] topic_patterns: Dict[str, Any] audience_insights: Dict[str, Any] full_analysis: Dict[str, Any] class ResearchResult(DBModel): """Research result model.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None query: str results: Dict[str, Any] suggested_topics: List[Dict[str, Any]] source: str = "perplexity" target_post_type_id: Optional[UUID] = None class ApiUsageLog(DBModel): """API usage log for tracking token usage and costs.""" id: Optional[UUID] = None user_id: Optional[UUID] = None provider: str # 'openai' | 'perplexity' model: str # 'gpt-4o', 'gpt-4o-mini', 'sonar' operation: str # 'post_creation', 'research', 'profile_analysis', etc. prompt_tokens: int = 0 completion_tokens: int = 0 total_tokens: int = 0 estimated_cost_usd: float = 0.0 created_at: Optional[datetime] = None class MediaItem(BaseModel): """Single media item (image or video) for a post.""" type: str # "image" | "video" url: str order: int content_type: str # MIME type (e.g., 'image/jpeg', 'video/mp4') uploaded_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) metadata: Optional[Dict[str, Any]] = None class GeneratedPost(DBModel): """Generated post model.""" id: Optional[UUID] = None user_id: UUID created_at: Optional[datetime] = None topic_id: Optional[UUID] = None topic_title: str post_content: str iterations: int = 0 writer_versions: List[str] = Field(default_factory=list) critic_feedback: List[Dict[str, Any]] = Field(default_factory=list) status: str = "draft" # draft, approved, ready, scheduled, published, rejected approved_at: Optional[datetime] = None published_at: Optional[datetime] = None post_type_id: Optional[UUID] = None # Media (multi-media support) media_items: List[MediaItem] = Field(default_factory=list) # DEPRECATED: Image (kept for backward compatibility) image_url: Optional[str] = None # Scheduling fields scheduled_at: Optional[datetime] = None scheduled_by_user_id: Optional[UUID] = None # Metadata for additional info (e.g., LinkedIn post URL, auto-posting status) metadata: Optional[Dict[str, Any]] = None @property def has_media(self) -> bool: """Check if post has any media items.""" return len(self.media_items) > 0 # ==================== LICENSE KEY MODELS ==================== class LicenseKey(DBModel): """License key for company registration.""" id: Optional[UUID] = None key: str description: Optional[str] = None # Limits max_employees: int = 5 daily_token_limit: Optional[int] = None # None = unlimited # Usage used: bool = False company_id: Optional[UUID] = None used_at: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class LicenseKeyOffer(DBModel): """MOCO offer created for a license key.""" id: Optional[UUID] = None license_key_id: UUID moco_offer_id: int moco_offer_identifier: Optional[str] = None moco_offer_url: Optional[str] = None offer_title: Optional[str] = None company_name: Optional[str] = None price: Optional[float] = None payment_frequency: Optional[str] = None status: str = "draft" created_at: Optional[datetime] = None class CompanyDailyQuota(DBModel): """Daily usage quota for a company.""" id: Optional[UUID] = None company_id: UUID date: date tokens_used: int = 0 created_at: Optional[datetime] = None updated_at: Optional[datetime] = None