418 lines
12 KiB
Python
418 lines
12 KiB
Python
"""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
|