Files
office_translator/models/subscription.py
2026-03-07 11:42:58 +01:00

349 lines
11 KiB
Python

"""
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 — March 2026):
# Essentielle : deepseek/deepseek-v3.2 ($0.25 / $0.38 per 1M tokens)
# Premium : anthropic/claude-3.5-haiku ($0.25 / $1.25 per 1M tokens)
# or google/gemini-3-flash ($0.15 / $0.60)
PLANS = {
PlanType.FREE: {
"name": "Gratuit",
"price_monthly": 0,
"price_yearly": 0,
"docs_per_month": 5,
"max_pages_per_doc": 15,
"max_file_size_mb": 5,
"max_chars_per_month": 50_000,
"providers": ["google"],
"features": [
"5 documents / mois",
"Jusqu'à 15 pages par document",
"Google Traduction inclus",
"Toutes les langues (130+)",
"Support communautaire",
],
"ai_translation": False,
"api_access": False,
"priority_processing": False,
"stripe_price_id_monthly": None,
"stripe_price_id_yearly": None,
"highlight": None,
"description": "Parfait pour découvrir l'application",
"badge": None,
},
PlanType.STARTER: {
"name": "Starter",
"price_monthly": 7.99,
"price_yearly": 76.70, # -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": [
"50 documents / mois",
"Jusqu'à 50 pages par document",
"Google Traduction + DeepL",
"Fichiers jusqu'à 10 Mo",
"Support par e-mail",
"Historique 30 jours",
],
"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": "Pour les particuliers et petits projets",
"badge": None,
},
PlanType.PRO: {
"name": "Pro",
"price_monthly": 19.99,
"price_yearly": 191.90, # -20 %
"docs_per_month": 200,
"max_pages_per_doc": 200,
"max_file_size_mb": 25,
"max_chars_per_month": 2_000_000,
"providers": ["google", "deepl", "openrouter"],
"ai_model_essential": "deepseek/deepseek-v3.2",
"features": [
"200 documents / mois",
"Jusqu'à 200 pages par document",
"Traduction IA Essentielle incluse (DeepSeek V3.2)",
"Google Traduction + DeepL",
"Fichiers jusqu'à 25 Mo",
"Glossaires personnalisés",
"Support prioritaire par e-mail",
"Historique 90 jours",
],
"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": "Le plus populaire",
"description": "Pour les professionnels et équipes en croissance",
"badge": "POPULAIRE",
},
PlanType.BUSINESS: {
"name": "Business",
"price_monthly": 49.99,
"price_yearly": 479.90, # -20 %
"docs_per_month": 1000,
"max_pages_per_doc": 500,
"max_file_size_mb": 50,
"max_chars_per_month": 10_000_000,
"providers": ["google", "deepl", "openrouter", "openrouter_premium", "openai", "zai"],
"ai_model_essential": "deepseek/deepseek-v3.2",
"ai_model_premium": "anthropic/claude-3.5-haiku",
"features": [
"1 000 documents / mois",
"Jusqu'à 500 pages par document",
"Traduction IA Essentielle + Premium (Claude Haiku)",
"Tous les fournisseurs de traduction",
"Fichiers jusqu'à 50 Mo",
"Accès API (10 000 appels/mois)",
"Webhooks de notification",
"Glossaires + Prompts personnalisés",
"Support dédié",
"Historique 1 an",
"Analytiques avancées",
],
"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": "Pour les équipes et organisations",
"badge": None,
},
PlanType.ENTERPRISE: {
"name": "Entreprise",
"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", "deepl", "openrouter", "openrouter_premium", "openai", "zai", "custom"],
"features": [
"Documents illimités",
"Tous les modèles IA (GPT-5, Claude Opus 4.6...)",
"Déploiement on-premise ou cloud dédié",
"SLA 99,9 % garanti",
"Support 24/7 dédié",
"Modèles IA personnalisés",
"Marque blanche (white-label)",
"Équipes illimitées",
"Intégrations sur mesure",
],
"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": "Solutions sur mesure pour grandes organisations",
"badge": "SUR DEVIS",
},
}
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
subscription_status: SubscriptionStatus = SubscriptionStatus.ACTIVE
stripe_customer_id: Optional[str] = None
stripe_subscription_id: Optional[str] = None
subscription_ends_at: Optional[datetime] = None
# 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"
# Ollama self-hosted config
ollama_endpoint: Optional[str] = None
ollama_model: Optional[str] = None
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("Le mot de passe doit contenir au moins 8 caractères")
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
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",
},
]