Merge pull request #499 from christianlouis/copilot/add-subscription-management-features
Merge main → subscription-management-features; fix migration chain collision
This commit is contained in:
+127
-5
@@ -1,9 +1,11 @@
|
||||
"""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)
|
||||
GET /api/subscriptions/tiers — list all available plans
|
||||
GET /api/subscriptions/my — current user's plan + usage (auth required)
|
||||
POST /api/subscriptions/change — request a plan change (auth required)
|
||||
DELETE /api/subscriptions/change — cancel a pending plan change (auth required)
|
||||
GET /api/subscriptions/platform — platform-wide stats (admin only)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,6 +13,7 @@ from datetime import datetime, timezone
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -19,10 +22,14 @@ from app.database import get_db
|
||||
from app.utils.subscription import (
|
||||
TIER_ORDER,
|
||||
TIERS,
|
||||
SubscriptionChangeError,
|
||||
apply_pending_subscription_changes,
|
||||
cancel_pending_subscription_change,
|
||||
get_all_tiers,
|
||||
get_tier,
|
||||
get_user_tier_id,
|
||||
get_user_usage,
|
||||
request_subscription_change,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,6 +40,37 @@ DbSession = Annotated[Session, Depends(get_db)]
|
||||
AdminUser = Annotated[dict, Depends(_require_admin)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request / response models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SubscriptionChangeRequest(BaseModel):
|
||||
"""Request body for a subscription plan change."""
|
||||
|
||||
plan_id: str
|
||||
billing_cycle: str = "monthly" # "monthly" | "yearly"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_owner_id(request: Request) -> str:
|
||||
"""Extract the authenticated user's owner_id from the session."""
|
||||
user = request.session.get("user") or {}
|
||||
return user.get("username") or user.get("email") or user.get("sub") or ""
|
||||
|
||||
|
||||
def _require_authenticated(request: Request) -> str:
|
||||
"""Return the owner_id or raise 401."""
|
||||
owner_id = _get_owner_id(request)
|
||||
if not owner_id:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
|
||||
return owner_id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -50,8 +88,12 @@ def list_tiers() -> dict[str, Any]:
|
||||
|
||||
@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."""
|
||||
"""Return the authenticated user's subscription tier and current usage counts.
|
||||
|
||||
Also applies any pending subscription changes that have become due.
|
||||
"""
|
||||
from app.config import settings
|
||||
from app.models import UserProfile
|
||||
|
||||
user = request.session.get("user")
|
||||
|
||||
@@ -67,18 +109,98 @@ def my_subscription(request: Request, db: DbSession) -> dict[str, Any]:
|
||||
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 ""
|
||||
|
||||
# Apply any pending change that has become due
|
||||
apply_pending_subscription_changes(db, owner_id)
|
||||
|
||||
tier_id = get_user_tier_id(db, owner_id)
|
||||
tier = get_tier(tier_id)
|
||||
tier = get_tier(tier_id, db)
|
||||
usage = get_user_usage(db, owner_id)
|
||||
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == owner_id).first()
|
||||
pending_tier_id: str | None = profile.subscription_change_pending_tier if profile else None
|
||||
pending_date: str | None = (
|
||||
profile.subscription_change_pending_date.isoformat()
|
||||
if profile and profile.subscription_change_pending_date
|
||||
else None
|
||||
)
|
||||
period_start: str | None = (
|
||||
profile.subscription_period_start.isoformat() if profile and profile.subscription_period_start else None
|
||||
)
|
||||
|
||||
return {
|
||||
"multi_user_mode": True,
|
||||
"owner_id": owner_id,
|
||||
"tier": tier,
|
||||
"usage": usage,
|
||||
"period_start": period_start,
|
||||
"pending_change": (
|
||||
{
|
||||
"tier_id": pending_tier_id,
|
||||
"tier": get_tier(pending_tier_id, db),
|
||||
"effective_date": pending_date,
|
||||
}
|
||||
if pending_tier_id
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/change", summary="Request a subscription plan change", status_code=status.HTTP_200_OK)
|
||||
def change_subscription(request: Request, body: SubscriptionChangeRequest, db: DbSession) -> dict[str, Any]:
|
||||
"""Request a subscription tier change.
|
||||
|
||||
**Upgrades** (moving to a higher-ranked plan) take effect immediately.
|
||||
|
||||
**Downgrades** (moving to a lower-ranked plan) are scheduled for the end
|
||||
of the current billing period to prevent gaming. The user keeps their
|
||||
current plan benefits until the scheduled date.
|
||||
|
||||
Requesting the currently active tier while a downgrade is pending cancels
|
||||
that pending change.
|
||||
"""
|
||||
from app.config import settings
|
||||
|
||||
if not settings.multi_user_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Subscription management is not available in single-user mode.",
|
||||
)
|
||||
|
||||
owner_id = _require_authenticated(request)
|
||||
|
||||
try:
|
||||
result = request_subscription_change(db, owner_id, body.plan_id, body.billing_cycle)
|
||||
except SubscriptionChangeError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/change", summary="Cancel a pending subscription change", status_code=status.HTTP_200_OK)
|
||||
def cancel_subscription_change(request: Request, db: DbSession) -> dict[str, Any]:
|
||||
"""Cancel a scheduled future subscription change.
|
||||
|
||||
Only downgrades can be pending; upgrades always take effect immediately.
|
||||
Returns 404 when there is no pending change to cancel.
|
||||
"""
|
||||
from app.config import settings
|
||||
|
||||
if not settings.multi_user_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Subscription management is not available in single-user mode.",
|
||||
)
|
||||
|
||||
owner_id = _require_authenticated(request)
|
||||
|
||||
cancelled = cancel_pending_subscription_change(db, owner_id)
|
||||
if not cancelled:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No pending subscription change found.")
|
||||
|
||||
return {"cancelled": True, "message": "Your pending subscription change has been cancelled."}
|
||||
|
||||
|
||||
@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)."""
|
||||
|
||||
@@ -25,6 +25,7 @@ from app.tasks.process_with_ocr import process_with_ocr # noqa: F401
|
||||
from app.tasks.refine_text_with_gpt import refine_text_with_gpt # noqa: F401
|
||||
from app.tasks.rotate_pdf_pages import rotate_pdf_pages # noqa: F401
|
||||
from app.tasks.send_to_all import send_to_all_destinations # noqa: F401
|
||||
from app.tasks.subscription_tasks import apply_pending_subscription_changes_all # noqa: F401
|
||||
|
||||
# Import new send tasks
|
||||
from app.tasks.upload_to_dropbox import upload_to_dropbox # noqa: F401
|
||||
@@ -104,6 +105,12 @@ celery.conf.beat_schedule = {
|
||||
"schedule": crontab(minute="*/5"), # Every 5 minutes
|
||||
"options": {"expires": 240}, # 4 minutes expiry
|
||||
},
|
||||
# Apply scheduled subscription downgrades daily at 00:05 UTC
|
||||
"apply-pending-subscription-changes": {
|
||||
"task": "app.tasks.subscription_tasks.apply_pending_subscription_changes_all",
|
||||
"schedule": crontab(hour="0", minute="5"), # 00:05 UTC daily
|
||||
"options": {"expires": 3600},
|
||||
},
|
||||
}
|
||||
|
||||
# Remove None entries from beat_schedule
|
||||
|
||||
@@ -238,6 +238,13 @@ class UserProfile(Base):
|
||||
subscription_period_start = Column(DateTime(timezone=True), nullable=True)
|
||||
allow_overage = Column(Boolean, nullable=False, default=False, server_default="0")
|
||||
|
||||
# Pending subscription change (added in migration 020_add_subscription_change_pending)
|
||||
# When a user requests a downgrade, the new tier is stored here and the
|
||||
# change is applied on `subscription_change_pending_date`. Upgrades are
|
||||
# applied immediately and these fields are left NULL.
|
||||
subscription_change_pending_tier = Column(String(50), nullable=True)
|
||||
subscription_change_pending_date = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# When True, the user is on a complimentary (uncharged) plan — they keep all tier
|
||||
# quota benefits but are never billed via Stripe. Automatically set for admin users.
|
||||
is_complimentary = Column(Boolean, nullable=False, default=False, server_default="0")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Celery task to apply pending subscription changes that have become due.
|
||||
|
||||
Runs daily to ensure that scheduled downgrades are applied on time.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from app.celery_app import celery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@celery.task(name="app.tasks.subscription_tasks.apply_pending_subscription_changes_all")
|
||||
def apply_pending_subscription_changes_all() -> dict[str, int]:
|
||||
"""Apply all pending subscription changes whose effective date has arrived.
|
||||
|
||||
Iterates over every ``UserProfile`` that has a pending change and calls
|
||||
:func:`app.utils.subscription.apply_pending_subscription_changes` for
|
||||
each one.
|
||||
|
||||
Returns:
|
||||
A dict with ``{"applied": <count>, "checked": <count>}``.
|
||||
"""
|
||||
from app.database import SessionLocal
|
||||
from app.models import UserProfile
|
||||
from app.utils.subscription import apply_pending_subscription_changes
|
||||
|
||||
applied = 0
|
||||
checked = 0
|
||||
db = SessionLocal()
|
||||
try:
|
||||
profiles = (
|
||||
db.query(UserProfile)
|
||||
.filter(
|
||||
UserProfile.subscription_change_pending_tier.isnot(None),
|
||||
UserProfile.subscription_change_pending_date.isnot(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for profile in profiles:
|
||||
checked += 1
|
||||
if apply_pending_subscription_changes(db, profile.user_id):
|
||||
applied += 1
|
||||
except Exception as exc:
|
||||
logger.error("Error in apply_pending_subscription_changes_all: %s", exc)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info("apply_pending_subscription_changes_all: checked=%d applied=%d", checked, applied)
|
||||
return {"checked": checked, "applied": applied}
|
||||
@@ -502,3 +502,279 @@ def get_user_usage(db: Session, owner_id: str) -> dict[str, int]:
|
||||
if profile and (profile.subscription_billing_cycle or "monthly") == "yearly" and profile.subscription_period_start:
|
||||
result["year_to_date"] = get_year_file_count(db, owner_id, profile.subscription_period_start)
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subscription change management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SubscriptionChangeError(Exception):
|
||||
"""Raised when a requested subscription change is not permitted."""
|
||||
|
||||
|
||||
def _tier_rank(tier_id: str) -> int:
|
||||
"""Return the numeric rank of *tier_id* (0 = free … 3 = business).
|
||||
|
||||
Unknown tier IDs are treated as rank 0 (free).
|
||||
"""
|
||||
try:
|
||||
return TIER_ORDER.index(tier_id)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def apply_pending_subscription_changes(db: Session, owner_id: str) -> bool:
|
||||
"""Apply any pending subscription change that is now due.
|
||||
|
||||
Checks whether the scheduled change date has arrived and, if so, applies
|
||||
the new tier immediately.
|
||||
|
||||
Args:
|
||||
db: Database session.
|
||||
owner_id: Stable user identifier.
|
||||
|
||||
Returns:
|
||||
``True`` if a pending change was applied, ``False`` otherwise.
|
||||
"""
|
||||
from app.models import UserProfile
|
||||
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == owner_id).first()
|
||||
if not profile:
|
||||
return False
|
||||
|
||||
pending_tier = profile.subscription_change_pending_tier
|
||||
pending_date = profile.subscription_change_pending_date
|
||||
if not pending_tier or not pending_date:
|
||||
return False
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
# Normalise pending_date to UTC-aware for comparison
|
||||
if pending_date.tzinfo is None:
|
||||
pending_date = pending_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
if now < pending_date:
|
||||
return False # Not yet due
|
||||
|
||||
old_tier = profile.subscription_tier or DEFAULT_TIER
|
||||
profile.subscription_tier = pending_tier
|
||||
profile.subscription_period_start = pending_date # New period started at change date
|
||||
profile.subscription_change_pending_tier = None
|
||||
profile.subscription_change_pending_date = None
|
||||
try:
|
||||
db.commit()
|
||||
logger.info(
|
||||
"Applied pending subscription change for %s: %s → %s",
|
||||
owner_id,
|
||||
old_tier,
|
||||
pending_tier,
|
||||
)
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.error("Failed to apply pending subscription change for %s: %s", owner_id, exc)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def request_subscription_change(
|
||||
db: Session,
|
||||
owner_id: str,
|
||||
new_tier_id: str,
|
||||
billing_cycle: str = "monthly",
|
||||
) -> dict[str, Any]:
|
||||
"""Process a user-initiated subscription change request.
|
||||
|
||||
Upgrade rules
|
||||
-------------
|
||||
Upgrades (moving to a higher-ranked tier) take effect **immediately**:
|
||||
the tier is switched and the period start is reset to *now*. Any
|
||||
previously scheduled downgrade is cancelled.
|
||||
|
||||
Downgrade rules
|
||||
---------------
|
||||
Downgrades (moving to a lower-ranked tier) are **always scheduled** for
|
||||
the end of the current billing period:
|
||||
|
||||
* If ``subscription_period_start`` is set and the period end is in the
|
||||
future, the change is queued for that date.
|
||||
* If there is no period start (e.g. admin-assigned tier), the period start
|
||||
is treated as *now* and the change is scheduled one month out.
|
||||
* If the period has already elapsed the change is applied immediately.
|
||||
|
||||
Cancelling a pending downgrade
|
||||
--------------------------------
|
||||
Requesting the *current* tier when there is a pending change cancels that
|
||||
pending change.
|
||||
|
||||
Args:
|
||||
db: Database session.
|
||||
owner_id: Stable user identifier.
|
||||
new_tier_id: Target plan ID (e.g. ``"starter"``).
|
||||
billing_cycle: ``"monthly"`` or ``"yearly"`` — stored on upgrade.
|
||||
|
||||
Returns:
|
||||
A dict with keys ``immediate`` (bool), ``effective_date`` (ISO-8601 str
|
||||
or ``None``), ``old_tier``, ``new_tier``, ``message``.
|
||||
|
||||
Raises:
|
||||
SubscriptionChangeError: If the requested change is not allowed.
|
||||
"""
|
||||
from app.models import UserProfile
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Validate target tier
|
||||
valid_ids = [t["id"] for t in get_all_tiers(db)]
|
||||
if new_tier_id not in valid_ids:
|
||||
raise SubscriptionChangeError(f"Unknown subscription plan: {new_tier_id!r}")
|
||||
|
||||
# Ensure profile row exists
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == owner_id).first()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=owner_id)
|
||||
db.add(profile)
|
||||
db.flush()
|
||||
|
||||
old_tier_id = profile.subscription_tier or DEFAULT_TIER
|
||||
|
||||
# Cancel pending change when user re-selects their current active tier
|
||||
if new_tier_id == old_tier_id:
|
||||
if profile.subscription_change_pending_tier:
|
||||
profile.subscription_change_pending_tier = None
|
||||
profile.subscription_change_pending_date = None
|
||||
db.commit()
|
||||
return {
|
||||
"immediate": True,
|
||||
"effective_date": None,
|
||||
"old_tier": old_tier_id,
|
||||
"new_tier": old_tier_id,
|
||||
"message": "Pending subscription change cancelled.",
|
||||
}
|
||||
raise SubscriptionChangeError("You are already on this plan.")
|
||||
|
||||
old_rank = _tier_rank(old_tier_id)
|
||||
new_rank = _tier_rank(new_tier_id)
|
||||
is_upgrade = new_rank > old_rank
|
||||
|
||||
if is_upgrade:
|
||||
# Apply immediately — reset period start
|
||||
profile.subscription_tier = new_tier_id
|
||||
profile.subscription_billing_cycle = billing_cycle
|
||||
profile.subscription_period_start = now
|
||||
# Cancel any previously scheduled downgrade
|
||||
profile.subscription_change_pending_tier = None
|
||||
profile.subscription_change_pending_date = None
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise SubscriptionChangeError("Failed to apply subscription upgrade.") from exc
|
||||
logger.info("Immediate upgrade for %s: %s → %s", owner_id, old_tier_id, new_tier_id)
|
||||
return {
|
||||
"immediate": True,
|
||||
"effective_date": None,
|
||||
"old_tier": old_tier_id,
|
||||
"new_tier": new_tier_id,
|
||||
"message": f"You have been upgraded to {get_tier(new_tier_id, db)['name']}. "
|
||||
"Your new limits are active immediately.",
|
||||
}
|
||||
|
||||
# --- Downgrade path ---
|
||||
# Determine end of the *first* billing period for the current plan.
|
||||
# Rule: a downgrade is immediate if the user has completed at least one
|
||||
# full month on the current plan; otherwise it is scheduled for the
|
||||
# end of that first month. This prevents gaming: a user who just
|
||||
# upgraded cannot immediately downgrade to avoid paying the first month.
|
||||
import calendar
|
||||
|
||||
period_start: datetime | None = profile.subscription_period_start
|
||||
if period_start is None:
|
||||
# No recorded start → treat today as start; schedule for one month out
|
||||
period_start = now
|
||||
profile.subscription_period_start = period_start
|
||||
|
||||
if period_start.tzinfo is None:
|
||||
period_start = period_start.replace(tzinfo=timezone.utc)
|
||||
|
||||
# End of the first billing month (same day next month, clamped to valid day)
|
||||
next_month_num = period_start.month % 12 + 1
|
||||
next_year = period_start.year + (1 if period_start.month == 12 else 0)
|
||||
max_day = calendar.monthrange(next_year, next_month_num)[1]
|
||||
next_day = min(period_start.day, max_day)
|
||||
change_date = period_start.replace(year=next_year, month=next_month_num, day=next_day)
|
||||
|
||||
if change_date <= now:
|
||||
profile.subscription_tier = new_tier_id
|
||||
profile.subscription_billing_cycle = billing_cycle
|
||||
profile.subscription_period_start = now
|
||||
profile.subscription_change_pending_tier = None
|
||||
profile.subscription_change_pending_date = None
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise SubscriptionChangeError("Failed to apply subscription downgrade.") from exc
|
||||
logger.info("Immediate downgrade for %s: %s → %s (period elapsed)", owner_id, old_tier_id, new_tier_id)
|
||||
return {
|
||||
"immediate": True,
|
||||
"effective_date": None,
|
||||
"old_tier": old_tier_id,
|
||||
"new_tier": new_tier_id,
|
||||
"message": f"Your subscription has been changed to {get_tier(new_tier_id, db)['name']}.",
|
||||
}
|
||||
|
||||
# Schedule the downgrade
|
||||
profile.subscription_change_pending_tier = new_tier_id
|
||||
profile.subscription_change_pending_date = change_date
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
raise SubscriptionChangeError("Failed to schedule subscription downgrade.") from exc
|
||||
|
||||
logger.info(
|
||||
"Scheduled downgrade for %s: %s → %s on %s",
|
||||
owner_id,
|
||||
old_tier_id,
|
||||
new_tier_id,
|
||||
change_date.isoformat(),
|
||||
)
|
||||
return {
|
||||
"immediate": False,
|
||||
"effective_date": change_date.isoformat(),
|
||||
"old_tier": old_tier_id,
|
||||
"new_tier": new_tier_id,
|
||||
"message": (
|
||||
f"Your downgrade to {get_tier(new_tier_id, db)['name']} has been scheduled for "
|
||||
f"{change_date.strftime('%B')} {change_date.day}, {change_date.year}. "
|
||||
"You will continue to have access to your current plan until then."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def cancel_pending_subscription_change(db: Session, owner_id: str) -> bool:
|
||||
"""Cancel a pending subscription change for *owner_id*.
|
||||
|
||||
Args:
|
||||
db: Database session.
|
||||
owner_id: Stable user identifier.
|
||||
|
||||
Returns:
|
||||
``True`` if a pending change was cancelled, ``False`` if there was nothing to cancel.
|
||||
"""
|
||||
from app.models import UserProfile
|
||||
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == owner_id).first()
|
||||
if not profile or not profile.subscription_change_pending_tier:
|
||||
return False
|
||||
|
||||
profile.subscription_change_pending_tier = None
|
||||
profile.subscription_change_pending_date = None
|
||||
try:
|
||||
db.commit()
|
||||
logger.info("Cancelled pending subscription change for %s", owner_id)
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.error("Failed to cancel pending subscription change for %s: %s", owner_id, exc)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -10,7 +10,14 @@ import logging
|
||||
from fastapi import Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.utils.subscription import TIER_ORDER, get_all_tiers, get_tier, get_user_tier_id, get_user_usage
|
||||
from app.utils.subscription import (
|
||||
TIER_ORDER,
|
||||
apply_pending_subscription_changes,
|
||||
get_all_tiers,
|
||||
get_tier,
|
||||
get_user_tier_id,
|
||||
get_user_usage,
|
||||
)
|
||||
from app.views.base import APIRouter, get_db, require_login, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,19 +43,31 @@ async def pricing_page(request: Request, db: Session = Depends(get_db)):
|
||||
async def my_subscription_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Authenticated user's subscription status and usage page."""
|
||||
from app.config import settings
|
||||
from app.models import UserProfile
|
||||
|
||||
user = request.session.get("user") or {}
|
||||
owner_id: str = user.get("username") or user.get("email") or user.get("sub") or ""
|
||||
|
||||
if settings.multi_user_enabled and owner_id:
|
||||
# Apply any pending changes that have become due before rendering
|
||||
apply_pending_subscription_changes(db, owner_id)
|
||||
|
||||
tier_id = get_user_tier_id(db, owner_id)
|
||||
usage = get_user_usage(db, owner_id)
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == owner_id).first()
|
||||
pending_tier_id = profile.subscription_change_pending_tier if profile else None
|
||||
pending_date = profile.subscription_change_pending_date if profile else None
|
||||
period_start = profile.subscription_period_start if profile else None
|
||||
else:
|
||||
tier_id = "business"
|
||||
usage = None
|
||||
pending_tier_id = None
|
||||
pending_date = None
|
||||
period_start = None
|
||||
|
||||
tier = get_tier(tier_id, db)
|
||||
all_tiers = get_all_tiers(db)
|
||||
pending_tier = get_tier(pending_tier_id, db) if pending_tier_id else None
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"subscription.html",
|
||||
@@ -60,5 +79,10 @@ async def my_subscription_page(request: Request, db: Session = Depends(get_db)):
|
||||
"all_tiers": all_tiers,
|
||||
"multi_user_enabled": settings.multi_user_enabled,
|
||||
"owner_id": owner_id,
|
||||
"tier_order": TIER_ORDER,
|
||||
"pending_tier_id": pending_tier_id,
|
||||
"pending_tier": pending_tier,
|
||||
"pending_date": pending_date,
|
||||
"period_start": period_start,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user