7f521eb755
- Use shared _require_admin from admin_users in subscriptions API endpoint
- Remove unnecessary Alpine.js hidden-div workaround in pricing.html
- Replace fragile string replace for OCR page count with proper Jinja {:,} format
- Improve comment wording in upload quota cleanup code
- Extract _scalar_count() helper in subscription.py to reduce repetition
- Add aria-valuemin='0' to all progressbar elements in subscription/index templates
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""API endpoints for subscription tiers and usage statistics.
|
|
|
|
Public endpoints:
|
|
GET /api/subscriptions/tiers — list all available plans
|
|
GET /api/subscriptions/my — current user's plan + usage (auth required)
|
|
GET /api/subscriptions/platform — platform-wide stats (admin only)
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Annotated, Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.admin_users import _require_admin
|
|
from app.database import get_db
|
|
from app.utils.subscription import (
|
|
TIER_ORDER,
|
|
TIERS,
|
|
get_all_tiers,
|
|
get_tier,
|
|
get_user_tier_id,
|
|
get_user_usage,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/subscriptions", tags=["subscriptions"])
|
|
|
|
DbSession = Annotated[Session, Depends(get_db)]
|
|
AdminUser = Annotated[dict, Depends(_require_admin)]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/tiers", summary="List all subscription tiers")
|
|
def list_tiers() -> dict[str, Any]:
|
|
"""Return the full list of subscription plans in display order."""
|
|
return {
|
|
"tiers": get_all_tiers(),
|
|
"order": TIER_ORDER,
|
|
"default": "free",
|
|
}
|
|
|
|
|
|
@router.get("/my", summary="Get current user's subscription and usage")
|
|
def my_subscription(request: Request, db: DbSession) -> dict[str, Any]:
|
|
"""Return the authenticated user's subscription tier and current usage counts."""
|
|
from app.config import settings
|
|
|
|
user = request.session.get("user")
|
|
|
|
if not settings.multi_user_enabled:
|
|
# In single-user mode there is no concept of a subscription plan
|
|
return {
|
|
"multi_user_mode": False,
|
|
"tier": TIERS["business"], # unrestricted
|
|
"usage": None,
|
|
}
|
|
|
|
if not user:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
|
|
|
|
owner_id: str = user.get("username") or user.get("email") or user.get("sub") or ""
|
|
tier_id = get_user_tier_id(db, owner_id)
|
|
tier = get_tier(tier_id)
|
|
usage = get_user_usage(db, owner_id)
|
|
|
|
return {
|
|
"multi_user_mode": True,
|
|
"owner_id": owner_id,
|
|
"tier": tier,
|
|
"usage": usage,
|
|
}
|
|
|
|
|
|
@router.get("/platform", summary="Platform-wide usage statistics (admin only)")
|
|
def platform_stats(request: Request, db: DbSession, _admin: AdminUser) -> dict[str, Any]:
|
|
"""Return aggregate statistics across all users and tiers (admin only)."""
|
|
from app.models import FileRecord, UserProfile
|
|
|
|
today = datetime.now(timezone.utc).date()
|
|
|
|
# Total files
|
|
total_files: int = db.query(func.count(FileRecord.id)).scalar() or 0
|
|
|
|
# Files today
|
|
files_today: int = (
|
|
db.query(func.count(FileRecord.id)).filter(func.date(FileRecord.created_at) == today).scalar() or 0
|
|
)
|
|
|
|
# Files this month
|
|
files_this_month: int = (
|
|
db.query(func.count(FileRecord.id))
|
|
.filter(func.strftime("%Y-%m", FileRecord.created_at) == today.strftime("%Y-%m"))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# Files with OCR text (proxy for pages OCRed — approximation)
|
|
files_with_ocr: int = db.query(func.count(FileRecord.id)).filter(FileRecord.ocr_text.isnot(None)).scalar() or 0
|
|
|
|
# Unique active users (ever uploaded)
|
|
unique_users: int = (
|
|
db.query(func.count(func.distinct(FileRecord.owner_id))).filter(FileRecord.owner_id.isnot(None)).scalar() or 0
|
|
)
|
|
|
|
# Users per subscription tier
|
|
profiles = (
|
|
db.query(UserProfile.subscription_tier, func.count(UserProfile.id))
|
|
.group_by(UserProfile.subscription_tier)
|
|
.all()
|
|
)
|
|
tier_distribution: dict[str, int] = {row[0] or "free": row[1] for row in profiles}
|
|
|
|
# Fill in zeros for tiers with no users
|
|
for tid in TIER_ORDER:
|
|
tier_distribution.setdefault(tid, 0)
|
|
|
|
return {
|
|
"files": {
|
|
"total": total_files,
|
|
"today": files_today,
|
|
"this_month": files_this_month,
|
|
"with_ocr": files_with_ocr,
|
|
},
|
|
"users": {
|
|
"unique_uploaders": unique_users,
|
|
"tier_distribution": tier_distribution,
|
|
},
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|