step 11 complete: billing service and tier manager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 22:41:35 +01:00
parent 8f7bc25611
commit 9787befd4a
7 changed files with 422 additions and 164 deletions

View File

@@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from pydantic import BaseModel
from app.api.deps import get_current_user
from app.billing.tier_manager import tier_manager
from app.schemas import StorageRecordCreate, StorageRecordUpdate, UserProfile
from app.storage.blob_store import BlobStore
from app.storage.encryption import reject_if_tampered
@@ -25,14 +26,6 @@ _blob_store = BlobStore()
# In-memory record metadata — replaced by PostgreSQL storage_records table in Step 12
_records: dict[str, dict[str, Any]] = {}
# TODO(Step11/12): replace with TierManager.check_quota(user_id)
_TIER_STORAGE_LIMITS_GB: dict[str, int] = {
"free": 0,
"pro": 5,
"power": 25,
"team": -1, # unlimited
}
# ── Local response schemas ─────────────────────────────────────────────
@@ -51,18 +44,10 @@ class _RecordMeta(BaseModel):
# ── Helpers ────────────────────────────────────────────────────────────
def _check_quota(user_id: str, tier: str, additional_bytes: int) -> None:
def _check_quota(user_id: str, additional_bytes: int) -> None:
"""Raise HTTP 402 if adding ``additional_bytes`` would exceed the tier limit."""
limit_gb = _TIER_STORAGE_LIMITS_GB.get(tier, 0)
if limit_gb == -1:
return # unlimited
limit_bytes = limit_gb * 1024**3
used = sum(r["size_bytes"] for r in _records.values() if r["user_id"] == user_id)
if used + additional_bytes > limit_bytes:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail=f"Storage quota exceeded for tier '{tier}'",
)
current = sum(r["size_bytes"] for r in _records.values() if r["user_id"] == user_id)
tier_manager.enforce_quota(user_id, current_bytes=current, additional_bytes=additional_bytes)
def _get_record_for_user(record_id: str, user_id: str) -> dict[str, Any]:
@@ -83,7 +68,7 @@ async def create_record(
) -> _CreateResponse:
"""Upload a new E2E-encrypted blob. Verifies checksum before storing."""
reject_if_tampered(body.blob, body.checksum)
_check_quota(current_user.id, current_user.tier, len(body.blob))
_check_quota(current_user.id, len(body.blob))
record_id = str(uuid.uuid4())
now = int(time.time() * 1000)
@@ -159,7 +144,7 @@ async def update_record(
delta = len(body.blob) - record["size_bytes"]
if delta > 0:
_check_quota(current_user.id, current_user.tier, delta)
_check_quota(current_user.id, delta)
s3_key = await _blob_store.upload(
current_user.id, record["table"], record_id, body.blob, body.checksum