mitai-jinkendo/backend/routers/tier_limits.py
Lars a849d5db9e
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
feat: add admin management routers for subscription system
Five new admin routers:

1. routers/features.py
   - GET/POST/PUT/DELETE /api/features
   - Feature registry CRUD
   - Allows adding new limitable features without schema changes

2. routers/tiers_mgmt.py
   - GET/POST/PUT/DELETE /api/tiers
   - Subscription tier management
   - Price configuration, sort order

3. routers/tier_limits.py
   - GET /api/tier-limits - Complete Tier x Feature matrix
   - PUT /api/tier-limits - Update single limit
   - PUT /api/tier-limits/batch - Batch update
   - DELETE /api/tier-limits - Remove limit (fallback to default)
   - Matrix editor backend

4. routers/user_restrictions.py
   - GET/POST/PUT/DELETE /api/user-restrictions
   - User-specific feature overrides
   - Highest priority in access hierarchy
   - Includes reason field for documentation

5. routers/access_grants.py
   - GET /api/access-grants - List grants with filters
   - POST /api/access-grants - Manual grant creation
   - PUT /api/access-grants/{id} - Extend/pause grants
   - DELETE /api/access-grants/{id} - Revoke access
   - Activity logging

All endpoints require admin authentication.
Completes backend API for v9c Phase 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 13:09:33 +01:00

159 lines
4.5 KiB
Python

"""
Tier Limits Management Endpoints for Mitai Jinkendo
Admin-only matrix editor for Tier x Feature limits.
"""
from fastapi import APIRouter, HTTPException, Depends
from db import get_db, get_cursor, r2d
from auth import require_admin
router = APIRouter(prefix="/api/tier-limits", tags=["tier-limits"])
@router.get("")
def get_tier_limits_matrix(session: dict = Depends(require_admin)):
"""
Admin: Get complete Tier x Feature matrix.
Returns:
{
"tiers": [{id, name}, ...],
"features": [{id, name, category}, ...],
"limits": {
"tier_id:feature_id": limit_value,
...
}
}
"""
with get_db() as conn:
cur = get_cursor(conn)
# Get all tiers
cur.execute("SELECT id, name, sort_order FROM tiers WHERE active = true ORDER BY sort_order")
tiers = [r2d(r) for r in cur.fetchall()]
# Get all features
cur.execute("""
SELECT id, name, category, limit_type, default_limit
FROM features
WHERE active = true
ORDER BY category, name
""")
features = [r2d(r) for r in cur.fetchall()]
# Get all tier_limits
cur.execute("SELECT tier_id, feature_id, limit_value FROM tier_limits")
limits = {}
for row in cur.fetchall():
key = f"{row['tier_id']}:{row['feature_id']}"
limits[key] = row['limit_value']
return {
"tiers": tiers,
"features": features,
"limits": limits
}
@router.put("")
def update_tier_limit(data: dict, session: dict = Depends(require_admin)):
"""
Admin: Update single tier limit.
Body:
{
"tier_id": "free",
"feature_id": "weight_entries",
"limit_value": 30 // NULL = unlimited, 0 = disabled
}
"""
tier_id = data.get('tier_id')
feature_id = data.get('feature_id')
limit_value = data.get('limit_value') # Can be None (NULL)
if not tier_id or not feature_id:
raise HTTPException(400, "tier_id und feature_id fehlen")
with get_db() as conn:
cur = get_cursor(conn)
# Upsert tier_limit
cur.execute("""
INSERT INTO tier_limits (tier_id, feature_id, limit_value)
VALUES (%s, %s, %s)
ON CONFLICT (tier_id, feature_id)
DO UPDATE SET
limit_value = EXCLUDED.limit_value,
updated = CURRENT_TIMESTAMP
""", (tier_id, feature_id, limit_value))
conn.commit()
return {"ok": True}
@router.put("/batch")
def update_tier_limits_batch(data: dict, session: dict = Depends(require_admin)):
"""
Admin: Batch update multiple tier limits.
Body:
{
"updates": [
{"tier_id": "free", "feature_id": "weight_entries", "limit_value": 30},
{"tier_id": "free", "feature_id": "ai_calls", "limit_value": 0},
...
]
}
"""
updates = data.get('updates', [])
if not updates:
raise HTTPException(400, "updates array fehlt")
with get_db() as conn:
cur = get_cursor(conn)
for update in updates:
tier_id = update.get('tier_id')
feature_id = update.get('feature_id')
limit_value = update.get('limit_value')
if not tier_id or not feature_id:
continue # Skip invalid entries
cur.execute("""
INSERT INTO tier_limits (tier_id, feature_id, limit_value)
VALUES (%s, %s, %s)
ON CONFLICT (tier_id, feature_id)
DO UPDATE SET
limit_value = EXCLUDED.limit_value,
updated = CURRENT_TIMESTAMP
""", (tier_id, feature_id, limit_value))
conn.commit()
return {"ok": True, "updated": len(updates)}
@router.delete("")
def delete_tier_limit(tier_id: str, feature_id: str, session: dict = Depends(require_admin)):
"""
Admin: Delete tier limit (falls back to feature default).
Query params: ?tier_id=...&feature_id=...
"""
if not tier_id or not feature_id:
raise HTTPException(400, "tier_id und feature_id fehlen")
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("""
DELETE FROM tier_limits
WHERE tier_id = %s AND feature_id = %s
""", (tier_id, feature_id))
conn.commit()
return {"ok": True}