# Feature Enforcement Pattern ## Übersicht Das Membership-System verwendet ein **4-Phasen-Modell** für Feature-Limits: 1. **Phase 1: Cleanup** - Feature-Konsolidierung, DB-Migration 2. **Phase 2: Non-blocking Monitoring** - JSON-Logging, keine Blockierung 3. **Phase 3: Frontend Display** - Usage-Badges, Quota-Übersicht 4. **Phase 4: Enforcement** - Tatsächliche Blockierung bei Limit-Überschreitung **Status (2026-03-21):** ✅ Alle 11 Features sind in Phase 4 (komplett implementiert) ## Wie neue Features hinzufügen ### 1. Feature in Datenbank registrieren ```sql INSERT INTO features (id, name, description, category, limit_type, reset_period, default_limit) VALUES ( 'new_feature_id', -- eindeutige ID 'Feature Name', -- Anzeigename 'Beschreibung', -- Beschreibung 'data', -- Kategorie: data, ai, export, integration 'count', -- limit_type: 'count' oder 'boolean' 'monthly', -- reset_period: 'never', 'daily', 'monthly' 50 -- default_limit: INT (NULL = unlimited, 0 = disabled) ); ``` **limit_type:** - `count`: Zählbare Features (z.B. 50 Einträge pro Monat) - `boolean`: An/Aus-Features (0 = disabled, 1 = enabled) **reset_period:** - `never`: Counter reset sich nie (z.B. Lifetime-Limits) - `daily`: Reset um Mitternacht - `monthly`: Reset am 1. des Monats ### 2. Backend-Implementierung #### a) Router-Endpoint anpassen ```python from auth import require_auth, check_feature_access, increment_feature_usage from feature_logger import log_feature_usage @router.post("/api/resource") def create_resource(data: dict, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Create new resource entry.""" pid = get_pid(x_profile_id) # Phase 4: Check feature access and ENFORCE access = check_feature_access(pid, 'new_feature_id') log_feature_usage(pid, 'new_feature_id', access, 'create') if not access['allowed']: logger.warning( f"[FEATURE-LIMIT] User {pid} blocked: " f"new_feature_id {access['reason']} (used: {access['used']}, limit: {access['limit']})" ) raise HTTPException( status_code=403, detail=f"Limit erreicht: Du hast das Kontingent für [Feature-Name] überschritten ({access['used']}/{access['limit']}). " f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset." ) # ... Business Logic: INSERT into DB ... # Phase 2: Increment usage counter (only for NEW entries, not updates!) increment_feature_usage(pid, 'new_feature_id') return {"id": resource_id} ``` #### b) Wichtige Regeln **Counter nur bei INSERT, nicht bei UPDATE:** ```python cur.execute("SELECT id FROM resource WHERE profile_id=%s AND date=%s", (pid, date)) existing = cur.fetchone() if existing: # UPDATE - NICHT incrementieren cur.execute("UPDATE resource SET ... WHERE id=%s", (..., existing['id'])) else: # INSERT - incrementieren cur.execute("INSERT INTO resource (...) VALUES (...)", (...)) increment_feature_usage(pid, 'new_feature_id') # ← Nur hier! ``` **Für Bulk-Operationen (CSV-Import):** ```python new_entries = 0 for row in csv_data: cur.execute("SELECT id FROM resource WHERE profile_id=%s AND date=%s", (pid, row['date'])) if not cur.fetchone(): # INSERT cur.execute("INSERT INTO resource (...) VALUES (...)", (...)) new_entries += 1 # Increment counter für alle neuen Einträge for _ in range(new_entries): increment_feature_usage(pid, 'new_feature_id') ``` ### 3. Frontend-Implementierung #### a) Import UsageBadge ```javascript import UsageBadge from '../components/UsageBadge' ``` #### b) State Management ```javascript export default function ResourcePage() { const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [error, setError] = useState(null) const [resourceUsage, setResourceUsage] = useState(null) // Phase 4 const load = () => api.listResources().then(setResources) const loadUsage = () => { api.getFeatureUsage().then(features => { const feature = features.find(f => f.feature_id === 'new_feature_id') setResourceUsage(feature) }).catch(err => console.error('Failed to load usage:', err)) } useEffect(() => { load() loadUsage() }, []) const handleSave = async () => { setSaving(true) setError(null) try { await api.createResource(data) setSaved(true) await load() await loadUsage() // Reload usage nach save setTimeout(() => setSaved(false), 2000) } catch (err) { console.error('Save failed:', err) setError(err.message || 'Fehler beim Speichern') setTimeout(() => setError(null), 5000) } finally { setSaving(false) } } ``` #### c) UI mit Badge und deaktiviertem Button ```javascript return (