All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m21s
348 lines
11 KiB
Python
348 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": "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-v3.2",
|
|
"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-v3.2",
|
|
"ai_model_premium": "anthropic/claude-3.5-haiku",
|
|
"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
|
|
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("Password must contain at least one uppercase letter")
|
|
if not re.search(r"[a-z]", v):
|
|
raise ValueError("Password must contain at least one lowercase letter")
|
|
if not re.search(r"[0-9]", v):
|
|
raise ValueError("Password must contain at least one digit")
|
|
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",
|
|
},
|
|
]
|