""" Subscription and User models for the monetization system """ from pydantic import BaseModel, EmailStr, Field, field_validator from typing import Optional, List, Dict, Any from datetime import datetime, timezone from enum import Enum import re class PlanType(str, Enum): FREE = "free" STARTER = "starter" PRO = "pro" BUSINESS = "business" ENTERPRISE = "enterprise" class SubscriptionStatus(str, Enum): ACTIVE = "active" CANCELED = "canceled" PAST_DUE = "past_due" TRIALING = "trialing" PAUSED = "paused" import os # Plan definitions — Pricing reviewed March 2026 # NOTE: Stripe price IDs must be set via env vars in production. # Create products/prices in Stripe Dashboard: https://dashboard.stripe.com/products # # LLM models used (via OpenRouter — June 2026): # Essentielle : deepseek/deepseek-chat (DeepSeek V4/V3) # Premium : anthropic/claude-sonnet-4.6 (Claude Sonnet 4.6) # or google/gemini-3.5-flash (Gemini 3.5 Flash) PLANS = { PlanType.FREE: { "name": "Free", "price_monthly": 0, "price_yearly": 0, "docs_per_month": 2, "max_pages_per_doc": 10, "max_file_size_mb": 5, "max_chars_per_month": 20_000, "providers": ["google"], "features": [ "pricing.plans.free.feat1", "pricing.plans.free.feat2", "pricing.plans.free.feat3", "pricing.plans.free.feat4", "pricing.plans.free.feat5", ], "ai_translation": False, "api_access": False, "priority_processing": False, "watermark": True, "stripe_price_id_monthly": None, "stripe_price_id_yearly": None, "highlight": None, "description": "pricing.plans.free.description", "badge": None, }, PlanType.STARTER: { "name": "Starter", "price_monthly": 9.00, "price_yearly": 86.40, # -20 % "docs_per_month": 50, "max_pages_per_doc": 50, "max_file_size_mb": 10, "max_chars_per_month": 500_000, "providers": ["google", "deepl"], "features": [ "pricing.plans.starter.feat1", "pricing.plans.starter.feat2", "pricing.plans.starter.feat3", "pricing.plans.starter.feat4", "pricing.plans.starter.feat5", "pricing.plans.starter.feat6", ], "ai_translation": False, "api_access": False, "priority_processing": False, "stripe_price_id_monthly": os.getenv("STRIPE_PRICE_STARTER_MONTHLY", ""), "stripe_price_id_yearly": os.getenv("STRIPE_PRICE_STARTER_YEARLY", ""), "highlight": None, "description": "pricing.plans.starter.description", "badge": None, }, PlanType.PRO: { "name": "Pro", "price_monthly": 19.00, "price_yearly": 182.40, # -20 % "docs_per_month": 200, "max_pages_per_doc": 200, "max_file_size_mb": 25, "max_chars_per_month": 2_000_000, "providers": ["google", "google_cloud", "deepl", "openrouter"], "ai_model_essential": "deepseek/deepseek-chat", "features": [ "pricing.plans.pro.feat1", "pricing.plans.pro.feat2", "pricing.plans.pro.feat3", "pricing.plans.pro.feat4", "pricing.plans.pro.feat5", "pricing.plans.pro.feat6", "pricing.plans.pro.feat7", "pricing.plans.pro.feat8", ], "ai_translation": True, "ai_tier": "essential", "api_access": False, "priority_processing": True, "stripe_price_id_monthly": os.getenv("STRIPE_PRICE_PRO_MONTHLY", ""), "stripe_price_id_yearly": os.getenv("STRIPE_PRICE_PRO_YEARLY", ""), "highlight": "pricing.plans.pro.highlight", "description": "pricing.plans.pro.description", "badge": "POPULAR", }, PlanType.BUSINESS: { "name": "Business", "price_monthly": 49.00, "price_yearly": 470.40, # -20 % "docs_per_month": 1000, "max_pages_per_doc": 500, "max_file_size_mb": 50, "max_chars_per_month": 10_000_000, "providers": ["google", "google_cloud", "deepl", "openrouter", "openrouter_premium", "openai", "zai"], "ai_model_essential": "deepseek/deepseek-chat", "ai_model_premium": "anthropic/claude-sonnet-4.6", "features": [ "pricing.plans.business.feat1", "pricing.plans.business.feat2", "pricing.plans.business.feat3", "pricing.plans.business.feat4", "pricing.plans.business.feat5", "pricing.plans.business.feat6", "pricing.plans.business.feat7", "pricing.plans.business.feat8", "pricing.plans.business.feat9", "pricing.plans.business.feat10", ], "ai_translation": True, "ai_tier": "premium", "api_access": True, "api_calls_per_month": 10_000, "priority_processing": True, "team_seats": 5, "stripe_price_id_monthly": os.getenv("STRIPE_PRICE_BUSINESS_MONTHLY", ""), "stripe_price_id_yearly": os.getenv("STRIPE_PRICE_BUSINESS_YEARLY", ""), "highlight": None, "description": "pricing.plans.business.description", "badge": None, }, PlanType.ENTERPRISE: { "name": "Enterprise", "price_monthly": -1, "price_yearly": -1, "docs_per_month": -1, "max_pages_per_doc": -1, "max_file_size_mb": -1, "max_chars_per_month": -1, "providers": ["google", "google_cloud", "deepl", "openrouter", "openrouter_premium", "openai", "zai", "custom"], "features": [ "pricing.plans.enterprise.feat1", "pricing.plans.enterprise.feat2", "pricing.plans.enterprise.feat3", "pricing.plans.enterprise.feat4", "pricing.plans.enterprise.feat5", "pricing.plans.enterprise.feat6", "pricing.plans.enterprise.feat7", "pricing.plans.enterprise.feat8", ], "ai_translation": True, "ai_tier": "custom", "api_access": True, "api_calls_per_month": -1, "priority_processing": True, "team_seats": -1, "stripe_price_id_monthly": None, "stripe_price_id_yearly": None, "highlight": None, "description": "pricing.plans.enterprise.description", "badge": "ON REQUEST", }, } def _utc_now() -> datetime: """Return current UTC datetime.""" return datetime.now(timezone.utc) class User(BaseModel): id: str email: EmailStr name: str password_hash: str created_at: datetime = Field(default_factory=_utc_now) updated_at: datetime = Field(default_factory=_utc_now) email_verified: bool = False avatar_url: Optional[str] = None # Subscription info plan: PlanType = PlanType.FREE tier: str = "free" # binary-ish tier for JWT/auth: free for free/starter, pro for paid subscription_status: SubscriptionStatus = SubscriptionStatus.ACTIVE stripe_customer_id: Optional[str] = None stripe_subscription_id: Optional[str] = None subscription_ends_at: Optional[datetime] = None cancel_at_period_end: bool = False # Usage tracking docs_translated_this_month: int = 0 pages_translated_this_month: int = 0 api_calls_this_month: int = 0 daily_translation_count: int = ( 0 # Daily count (reset at midnight UTC; synced with tier quota) ) usage_reset_date: datetime = Field(default_factory=_utc_now) # Extra credits (purchased separately) extra_credits: int = 0 # Each credit = 1 page # Settings default_source_lang: str = "auto" default_target_lang: str = "en" default_provider: str = "google" class UserCreate(BaseModel): email: EmailStr name: str = Field(..., min_length=1, max_length=100) password: str = Field(..., min_length=8) @field_validator("password") @classmethod def validate_password_strength(cls, v: str) -> str: """Validate password meets minimum security requirements.""" if len(v) < 8: raise ValueError("Password must be at least 8 characters") if not re.search(r"[A-Z]", v): raise ValueError("Le mot de passe doit contenir au moins une majuscule") if not re.search(r"[a-z]", v): raise ValueError("Le mot de passe doit contenir au moins une minuscule") if not re.search(r"[0-9]", v): raise ValueError("Le mot de passe doit contenir au moins un chiffre") return v class UserLogin(BaseModel): email: EmailStr password: str class UserResponse(BaseModel): id: str email: EmailStr name: str avatar_url: Optional[str] = None plan: PlanType tier: PlanType subscription_status: SubscriptionStatus subscription_ends_at: Optional[datetime] = None cancel_at_period_end: bool = False docs_translated_this_month: int pages_translated_this_month: int api_calls_this_month: int extra_credits: int created_at: datetime plan_limits: Dict[str, Any] = {} class Subscription(BaseModel): id: str user_id: str plan: PlanType status: SubscriptionStatus stripe_subscription_id: Optional[str] = None stripe_customer_id: Optional[str] = None current_period_start: datetime current_period_end: datetime cancel_at_period_end: bool = False created_at: datetime = Field(default_factory=_utc_now) class UsageRecord(BaseModel): id: str user_id: str document_name: str document_type: str # excel, word, pptx pages_count: int source_lang: str target_lang: str provider: str processing_time_seconds: float credits_used: int created_at: datetime = Field(default_factory=_utc_now) class CreditPurchase(BaseModel): """For buying extra credits (pay-per-use)""" id: str user_id: str credits_amount: int price_paid: float # in cents stripe_payment_id: str created_at: datetime = Field(default_factory=_utc_now) # Credit packages for purchase CREDIT_PACKAGES = [ { "credits": 50, "price": 5.00, "price_per_credit": 0.10, "stripe_price_id": "price_credits_50", }, { "credits": 100, "price": 9.00, "price_per_credit": 0.09, "stripe_price_id": "price_credits_100", "popular": True, }, { "credits": 250, "price": 20.00, "price_per_credit": 0.08, "stripe_price_id": "price_credits_250", }, { "credits": 500, "price": 35.00, "price_per_credit": 0.07, "stripe_price_id": "price_credits_500", }, { "credits": 1000, "price": 60.00, "price_per_credit": 0.06, "stripe_price_id": "price_credits_1000", }, ]