feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle

Made-with: Cursor
This commit is contained in:
Sepehr Ramezani
2026-03-07 11:42:58 +01:00
parent 3d37ce4582
commit 473b3e26c7
181 changed files with 30617 additions and 7170 deletions

View File

@@ -2,54 +2,64 @@
Repository layer for database operations
Provides clean interface for CRUD operations
"""
import hashlib
import secrets
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import and_, func, or_
from database.models import (
User, Translation, ApiKey, UsageLog, PaymentHistory,
PlanType, SubscriptionStatus
User,
Translation,
ApiKey,
UsageLog,
PaymentHistory,
PlanType,
SubscriptionStatus,
)
class UserRepository:
"""Repository for User database operations"""
def __init__(self, db: Session):
self.db = db
def get_by_id(self, user_id: str) -> Optional[User]:
"""Get user by ID"""
return self.db.query(User).filter(User.id == user_id).first()
def get_by_email(self, email: str) -> Optional[User]:
"""Get user by email (case-insensitive)"""
return self.db.query(User).filter(
func.lower(User.email) == email.lower()
).first()
return (
self.db.query(User).filter(func.lower(User.email) == email.lower()).first()
)
def get_by_stripe_customer(self, stripe_customer_id: str) -> Optional[User]:
"""Get user by Stripe customer ID"""
return self.db.query(User).filter(
User.stripe_customer_id == stripe_customer_id
).first()
return (
self.db.query(User)
.filter(User.stripe_customer_id == stripe_customer_id)
.first()
)
def create(
self,
email: str,
name: str,
password_hash: str,
plan: PlanType = PlanType.FREE
self,
email: str,
name: str,
hashed_password: str,
tier: str = "free",
) -> User:
"""Create a new user"""
"""Create a new user. Uses hashed_password and tier (story 1-1 refactor)."""
plan = PlanType.PRO if tier == "pro" else PlanType.FREE
user = User(
email=email.lower(),
name=name,
password_hash=password_hash,
hashed_password=hashed_password,
tier=tier,
plan=plan,
subscription_status=SubscriptionStatus.ACTIVE,
)
@@ -57,94 +67,90 @@ class UserRepository:
self.db.commit()
self.db.refresh(user)
return user
def update(self, user_id: str, **kwargs) -> Optional[User]:
"""Update user fields"""
user = self.get_by_id(user_id)
if not user:
return None
for key, value in kwargs.items():
if hasattr(user, key):
setattr(user, key, value)
user.updated_at = datetime.utcnow()
user.updated_at = datetime.now(timezone.utc)
self.db.commit()
self.db.refresh(user)
return user
def delete(self, user_id: str) -> bool:
"""Delete a user"""
user = self.get_by_id(user_id)
if not user:
return False
self.db.delete(user)
self.db.commit()
return True
def increment_usage(
self,
user_id: str,
docs: int = 0,
pages: int = 0,
api_calls: int = 0
self, user_id: str, docs: int = 0, pages: int = 0, api_calls: int = 0
) -> Optional[User]:
"""Increment usage counters"""
user = self.get_by_id(user_id)
if not user:
return None
# Check if usage needs to be reset (monthly)
if user.usage_reset_date:
now = datetime.utcnow()
if now.month != user.usage_reset_date.month or now.year != user.usage_reset_date.year:
now = datetime.now(timezone.utc)
if (
now.month != user.usage_reset_date.month
or now.year != user.usage_reset_date.year
):
user.docs_translated_this_month = 0
user.pages_translated_this_month = 0
user.api_calls_this_month = 0
user.usage_reset_date = now
user.docs_translated_this_month += docs
user.pages_translated_this_month += pages
user.api_calls_this_month += api_calls
self.db.commit()
self.db.refresh(user)
return user
def add_credits(self, user_id: str, credits: int) -> Optional[User]:
"""Add extra credits to user"""
user = self.get_by_id(user_id)
if not user:
return None
user.extra_credits += credits
self.db.commit()
self.db.refresh(user)
return user
def use_credits(self, user_id: str, credits: int) -> bool:
"""Use credits from user balance"""
user = self.get_by_id(user_id)
if not user or user.extra_credits < credits:
return False
user.extra_credits -= credits
self.db.commit()
return True
def get_all_users(
self,
skip: int = 0,
limit: int = 100,
plan: Optional[PlanType] = None
self, skip: int = 0, limit: int = 100, plan: Optional[PlanType] = None
) -> List[User]:
"""Get all users with pagination"""
query = self.db.query(User)
if plan:
query = query.filter(User.plan == plan)
return query.offset(skip).limit(limit).all()
def count_users(self, plan: Optional[PlanType] = None) -> int:
"""Count total users"""
query = self.db.query(func.count(User.id))
@@ -155,10 +161,10 @@ class UserRepository:
class TranslationRepository:
"""Repository for Translation database operations"""
def __init__(self, db: Session):
self.db = db
def create(
self,
user_id: str,
@@ -186,7 +192,36 @@ class TranslationRepository:
self.db.commit()
self.db.refresh(translation)
return translation
def create_completed(
self,
user_id: str,
original_filename: str,
file_type: str,
target_language: str,
provider: str,
source_language: str = "auto",
file_size_bytes: int = 0,
page_count: int = 0,
) -> Translation:
"""Create a translation record directly in completed status (Story 1.8 - billing log)."""
translation = Translation(
user_id=user_id,
original_filename=original_filename,
file_type=file_type,
file_size_bytes=file_size_bytes,
page_count=page_count,
source_language=source_language,
target_language=target_language,
provider=provider,
status="completed",
completed_at=datetime.now(timezone.utc),
)
self.db.add(translation)
self.db.commit()
self.db.refresh(translation)
return translation
def update_status(
self,
translation_id: str,
@@ -196,13 +231,13 @@ class TranslationRepository:
characters_translated: Optional[int] = None,
) -> Optional[Translation]:
"""Update translation status"""
translation = self.db.query(Translation).filter(
Translation.id == translation_id
).first()
translation = (
self.db.query(Translation).filter(Translation.id == translation_id).first()
)
if not translation:
return None
translation.status = status
if error_message:
translation.error_message = error_message
@@ -211,12 +246,12 @@ class TranslationRepository:
if characters_translated:
translation.characters_translated = characters_translated
if status == "completed":
translation.completed_at = datetime.utcnow()
translation.completed_at = datetime.now(timezone.utc)
self.db.commit()
self.db.refresh(translation)
return translation
def get_user_translations(
self,
user_id: str,
@@ -228,24 +263,33 @@ class TranslationRepository:
query = self.db.query(Translation).filter(Translation.user_id == user_id)
if status:
query = query.filter(Translation.status == status)
return query.order_by(Translation.created_at.desc()).offset(skip).limit(limit).all()
return (
query.order_by(Translation.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
def get_user_stats(self, user_id: str, days: int = 30) -> Dict[str, Any]:
"""Get user's translation statistics"""
since = datetime.utcnow() - timedelta(days=days)
result = self.db.query(
func.count(Translation.id).label("total_translations"),
func.sum(Translation.page_count).label("total_pages"),
func.sum(Translation.characters_translated).label("total_characters"),
).filter(
and_(
Translation.user_id == user_id,
Translation.created_at >= since,
Translation.status == "completed",
since = datetime.now(timezone.utc) - timedelta(days=days)
result = (
self.db.query(
func.count(Translation.id).label("total_translations"),
func.sum(Translation.page_count).label("total_pages"),
func.sum(Translation.characters_translated).label("total_characters"),
)
).first()
.filter(
and_(
Translation.user_id == user_id,
Translation.created_at >= since,
Translation.status == "completed",
)
)
.first()
)
return {
"total_translations": result.total_translations or 0,
"total_pages": result.total_pages or 0,
@@ -256,15 +300,15 @@ class TranslationRepository:
class ApiKeyRepository:
"""Repository for API Key database operations"""
def __init__(self, db: Session):
self.db = db
@staticmethod
def hash_key(key: str) -> str:
"""Hash an API key"""
return hashlib.sha256(key.encode()).hexdigest()
def create(
self,
user_id: str,
@@ -277,11 +321,11 @@ class ApiKeyRepository:
raw_key = f"tr_{secrets.token_urlsafe(32)}"
key_hash = self.hash_key(raw_key)
key_prefix = raw_key[:10]
expires_at = None
if expires_in_days:
expires_at = datetime.utcnow() + timedelta(days=expires_in_days)
expires_at = datetime.now(timezone.utc) + timedelta(days=expires_in_days)
api_key = ApiKey(
user_id=user_id,
name=name,
@@ -293,49 +337,60 @@ class ApiKeyRepository:
self.db.add(api_key)
self.db.commit()
self.db.refresh(api_key)
return api_key, raw_key
def get_by_key(self, raw_key: str) -> Optional[ApiKey]:
"""Get API key by raw key value"""
key_hash = self.hash_key(raw_key)
api_key = self.db.query(ApiKey).filter(
and_(
ApiKey.key_hash == key_hash,
ApiKey.is_active == True,
api_key = (
self.db.query(ApiKey)
.filter(
and_(
ApiKey.key_hash == key_hash,
ApiKey.is_active == True,
)
)
).first()
.first()
)
if api_key:
# Check expiration
if api_key.expires_at and api_key.expires_at < datetime.utcnow():
if api_key.expires_at and api_key.expires_at < datetime.now(timezone.utc):
return None
# Update last used
api_key.last_used_at = datetime.utcnow()
api_key.last_used_at = datetime.now(timezone.utc)
api_key.usage_count += 1
self.db.commit()
return api_key
def get_user_keys(self, user_id: str) -> List[ApiKey]:
"""Get all API keys for a user"""
return self.db.query(ApiKey).filter(
ApiKey.user_id == user_id
).order_by(ApiKey.created_at.desc()).all()
return (
self.db.query(ApiKey)
.filter(ApiKey.user_id == user_id)
.order_by(ApiKey.created_at.desc())
.all()
)
def revoke(self, key_id: str, user_id: str) -> bool:
"""Revoke an API key"""
api_key = self.db.query(ApiKey).filter(
and_(
ApiKey.id == key_id,
ApiKey.user_id == user_id,
api_key = (
self.db.query(ApiKey)
.filter(
and_(
ApiKey.id == key_id,
ApiKey.user_id == user_id,
)
)
).first()
.first()
)
if not api_key:
return False
api_key.is_active = False
self.db.commit()
return True