""" Feature Management Endpoints for Mitai Jinkendo Admin-only CRUD for features registry. User endpoint for feature usage overview (Phase 3). """ from typing import Optional from datetime import datetime from fastapi import APIRouter, HTTPException, Header, Depends from db import get_db, get_cursor, r2d from auth import require_admin, require_auth, check_feature_access from routers.profiles import get_pid router = APIRouter(prefix="/api/features", tags=["features"]) @router.get("") def list_features(session: dict = Depends(require_admin)): """Admin: List all features.""" with get_db() as conn: cur = get_cursor(conn) cur.execute(""" SELECT * FROM features ORDER BY category, name """) return [r2d(r) for r in cur.fetchall()] @router.post("") def create_feature(data: dict, session: dict = Depends(require_admin)): """ Admin: Create new feature. Required fields: - id: Feature ID (e.g., 'new_data_source') - name: Display name - category: 'data', 'ai', 'export', 'integration' - limit_type: 'count' or 'boolean' - reset_period: 'never', 'daily', 'monthly' - default_limit: INT or NULL (unlimited) """ feature_id = data.get('id', '').strip() name = data.get('name', '').strip() description = data.get('description', '') category = data.get('category') limit_type = data.get('limit_type', 'count') reset_period = data.get('reset_period', 'never') default_limit = data.get('default_limit') if not feature_id or not name: raise HTTPException(400, "ID und Name fehlen") if category not in ['data', 'ai', 'export', 'integration']: raise HTTPException(400, "Ungültige Kategorie") if limit_type not in ['count', 'boolean']: raise HTTPException(400, "limit_type muss 'count' oder 'boolean' sein") if reset_period not in ['never', 'daily', 'monthly']: raise HTTPException(400, "Ungültiger reset_period") with get_db() as conn: cur = get_cursor(conn) # Check if ID already exists cur.execute("SELECT id FROM features WHERE id = %s", (feature_id,)) if cur.fetchone(): raise HTTPException(400, f"Feature '{feature_id}' existiert bereits") # Create feature cur.execute(""" INSERT INTO features ( id, name, description, category, limit_type, reset_period, default_limit ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, (feature_id, name, description, category, limit_type, reset_period, default_limit)) conn.commit() return {"ok": True, "id": feature_id} @router.put("/{feature_id}") def update_feature(feature_id: str, data: dict, session: dict = Depends(require_admin)): """Admin: Update feature.""" with get_db() as conn: cur = get_cursor(conn) updates = [] values = [] if 'name' in data: updates.append('name = %s') values.append(data['name']) if 'description' in data: updates.append('description = %s') values.append(data['description']) if 'default_limit' in data: updates.append('default_limit = %s') values.append(data['default_limit']) if 'active' in data: updates.append('active = %s') values.append(data['active']) if not updates: return {"ok": True} updates.append('updated = CURRENT_TIMESTAMP') values.append(feature_id) cur.execute( f"UPDATE features SET {', '.join(updates)} WHERE id = %s", values ) conn.commit() return {"ok": True} @router.delete("/{feature_id}") def delete_feature(feature_id: str, session: dict = Depends(require_admin)): """Admin: Delete feature (soft-delete: set active=false).""" with get_db() as conn: cur = get_cursor(conn) cur.execute("UPDATE features SET active = false WHERE id = %s", (feature_id,)) conn.commit() return {"ok": True} @router.get("/{feature_id}/check-access") def check_access(feature_id: str, session: dict = Depends(require_auth)): """ User: Check if current user can access a feature. Returns: - allowed: bool - whether user can use the feature - limit: int|null - total limit (null = unlimited) - used: int - current usage - remaining: int|null - remaining uses (null = unlimited) - reason: str - why access is granted/denied """ profile_id = session['profile_id'] result = check_feature_access(profile_id, feature_id) return result @router.get("/usage") def get_feature_usage(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """ User: Get usage overview for all active features (Phase 3: Frontend Display). Returns list of all features with current usage, limits, and reset info. Automatically includes new features from database - no code changes needed. Response: [ { "feature_id": "weight_entries", "name": "Gewichtseinträge", "description": "Anzahl der Gewichtseinträge", "category": "data", "limit_type": "count", "reset_period": "never", "used": 5, "limit": 10, "remaining": 5, "allowed": true, "reset_at": null }, ... ] """ pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) # Get all active features (dynamic - picks up new features automatically) cur.execute(""" SELECT id, name, description, category, limit_type, reset_period FROM features WHERE active = true ORDER BY category, name """) features = [r2d(r) for r in cur.fetchall()] result = [] for feature in features: # Use existing check_feature_access to get usage and limits # This respects user overrides, tier limits, and feature defaults # Pass connection to avoid pool exhaustion access = check_feature_access(pid, feature['id'], conn) # Get reset date from user_feature_usage cur.execute(""" SELECT reset_at FROM user_feature_usage WHERE profile_id = %s AND feature_id = %s """, (pid, feature['id'])) usage_row = cur.fetchone() # Format reset_at as ISO string reset_at = None if usage_row and usage_row['reset_at']: if isinstance(usage_row['reset_at'], datetime): reset_at = usage_row['reset_at'].isoformat() else: reset_at = str(usage_row['reset_at']) result.append({ 'feature_id': feature['id'], 'name': feature['name'], 'description': feature.get('description'), 'category': feature.get('category'), 'limit_type': feature['limit_type'], 'reset_period': feature['reset_period'], 'used': access['used'], 'limit': access['limit'], 'remaining': access['remaining'], 'allowed': access['allowed'], 'reset_at': reset_at }) return result