feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
"""
|
||||
Subscription and User models for the monetization system
|
||||
"""
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
import re
|
||||
|
||||
|
||||
class PlanType(str, Enum):
|
||||
@@ -22,157 +24,209 @@ class SubscriptionStatus(str, Enum):
|
||||
TRIALING = "trialing"
|
||||
PAUSED = "paused"
|
||||
|
||||
|
||||
import os
|
||||
|
||||
# Plan definitions with limits
|
||||
# NOTE: Stripe price IDs should be set via environment variables in production
|
||||
# Create products and prices in Stripe Dashboard: https://dashboard.stripe.com/products
|
||||
# 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": "Free",
|
||||
"name": "Gratuit",
|
||||
"price_monthly": 0,
|
||||
"price_yearly": 0,
|
||||
"docs_per_month": 3,
|
||||
"max_pages_per_doc": 10,
|
||||
"docs_per_month": 5,
|
||||
"max_pages_per_doc": 15,
|
||||
"max_file_size_mb": 5,
|
||||
"providers": ["ollama"], # Only self-hosted
|
||||
"max_chars_per_month": 50_000,
|
||||
"providers": ["google"],
|
||||
"features": [
|
||||
"3 documents per day",
|
||||
"Up to 10 pages per document",
|
||||
"Ollama (self-hosted) only",
|
||||
"Basic support via community",
|
||||
"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": 12, # Updated pricing
|
||||
"price_yearly": 120, # 2 months free
|
||||
"price_monthly": 7.99,
|
||||
"price_yearly": 76.70, # -20 %
|
||||
"docs_per_month": 50,
|
||||
"max_pages_per_doc": 50,
|
||||
"max_file_size_mb": 25,
|
||||
"providers": ["ollama", "google", "libre"],
|
||||
"max_file_size_mb": 10,
|
||||
"max_chars_per_month": 500_000,
|
||||
"providers": ["google", "deepl"],
|
||||
"features": [
|
||||
"50 documents per month",
|
||||
"Up to 50 pages per document",
|
||||
"Google Translate included",
|
||||
"LibreTranslate included",
|
||||
"Email support",
|
||||
"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": 39, # Updated pricing
|
||||
"price_yearly": 390, # 2 months free
|
||||
"price_monthly": 19.99,
|
||||
"price_yearly": 191.90, # -20 %
|
||||
"docs_per_month": 200,
|
||||
"max_pages_per_doc": 200,
|
||||
"max_file_size_mb": 100,
|
||||
"providers": ["ollama", "google", "deepl", "openai", "libre", "openrouter"],
|
||||
"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 per month",
|
||||
"Up to 200 pages per document",
|
||||
"All translation providers",
|
||||
"DeepL & OpenAI included",
|
||||
"API access (1000 calls/month)",
|
||||
"Priority email support",
|
||||
"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",
|
||||
],
|
||||
"api_access": True,
|
||||
"api_calls_per_month": 1000,
|
||||
"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": 99, # Updated pricing
|
||||
"price_yearly": 990, # 2 months free
|
||||
"price_monthly": 49.99,
|
||||
"price_yearly": 479.90, # -20 %
|
||||
"docs_per_month": 1000,
|
||||
"max_pages_per_doc": 500,
|
||||
"max_file_size_mb": 250,
|
||||
"providers": ["ollama", "google", "deepl", "openai", "libre", "openrouter", "azure"],
|
||||
"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": [
|
||||
"1000 documents per month",
|
||||
"Up to 500 pages per document",
|
||||
"All translation providers",
|
||||
"Azure Translator included",
|
||||
"Unlimited API access",
|
||||
"Priority processing queue",
|
||||
"Dedicated support",
|
||||
"Team management (up to 5 users)",
|
||||
"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": -1, # Unlimited
|
||||
"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": "Enterprise",
|
||||
"price_monthly": -1, # Custom
|
||||
"name": "Entreprise",
|
||||
"price_monthly": -1,
|
||||
"price_yearly": -1,
|
||||
"docs_per_month": -1, # Unlimited
|
||||
"docs_per_month": -1,
|
||||
"max_pages_per_doc": -1,
|
||||
"max_file_size_mb": -1,
|
||||
"providers": ["ollama", "google", "deepl", "openai", "libre", "openrouter", "azure", "custom"],
|
||||
"max_chars_per_month": -1,
|
||||
"providers": ["google", "deepl", "openrouter", "openrouter_premium", "openai", "zai", "custom"],
|
||||
"features": [
|
||||
"Unlimited documents",
|
||||
"Unlimited pages",
|
||||
"Custom integrations",
|
||||
"On-premise deployment",
|
||||
"SLA guarantee",
|
||||
"24/7 dedicated support",
|
||||
"Custom AI models",
|
||||
"White-label option",
|
||||
"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, # Unlimited
|
||||
"stripe_price_id_monthly": None, # Contact sales
|
||||
"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=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
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
|
||||
usage_reset_date: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
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
|
||||
@@ -180,8 +234,22 @@ class User(BaseModel):
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
name: str
|
||||
password: str
|
||||
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):
|
||||
@@ -195,14 +263,14 @@ class UserResponse(BaseModel):
|
||||
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 for display
|
||||
|
||||
plan_limits: Dict[str, Any] = {}
|
||||
|
||||
|
||||
@@ -216,7 +284,7 @@ class Subscription(BaseModel):
|
||||
current_period_start: datetime
|
||||
current_period_end: datetime
|
||||
cancel_at_period_end: bool = False
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=_utc_now)
|
||||
|
||||
|
||||
class UsageRecord(BaseModel):
|
||||
@@ -230,24 +298,51 @@ class UsageRecord(BaseModel):
|
||||
provider: str
|
||||
processing_time_seconds: float
|
||||
credits_used: int
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
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=datetime.utcnow)
|
||||
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"},
|
||||
{
|
||||
"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",
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user