# Feature Enforcement Mapping **Version:** v9c Phase 2 **Status:** Planning **Datum:** 20. März 2026 --- ## Übersicht Dieses Dokument definiert, welche API-Endpoints welche Features prüfen müssen. --- ## Feature-Katalog (nach Cleanup) ### Data Features (count, never) 1. `weight_entries` - Gewichtseinträge 2. `circumference_entries` - Umfangsmessungen 3. `caliper_entries` - Hautfaltenmessungen 4. `nutrition_entries` - Ernährungseinträge 5. `activity_entries` - Trainingseinträge 6. `photos` - Progress-Fotos ### AI Features 7. `ai_calls` - KI-Einzelanalysen (count, monthly) 8. `ai_pipeline` - KI-Pipeline-Analyse (boolean, never) ### Export/Import Features 9. `data_export` - Daten exportieren (count, monthly) 10. `data_import` - Daten importieren (count, monthly) --- ## Endpoint → Feature Mapping ### Weight Router (`/api/weight`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/weight` | POST | `weight_entries` | Check before create, increment after | | `/api/weight` | GET | - | No check (reading is always allowed) | | `/api/weight/{id}` | PUT | - | No check (editing existing is allowed) | | `/api/weight/{id}` | DELETE | - | No check (deleting is allowed) | **Rationale:** Limit bezieht sich auf Gesamtanzahl Einträge (COUNT), nicht auf API-Calls. --- ### Circumference Router (`/api/circumference`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/circumference` | POST | `circumference_entries` | Check before, increment after | | `/api/circumference` | GET | - | No check | | `/api/circumference/{id}` | PUT | - | No check | | `/api/circumference/{id}` | DELETE | - | No check | --- ### Caliper Router (`/api/caliper`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/caliper` | POST | `caliper_entries` | Check before, increment after | | `/api/caliper` | GET | - | No check | | `/api/caliper/{id}` | PUT | - | No check | | `/api/caliper/{id}` | DELETE | - | No check | --- ### Nutrition Router (`/api/nutrition`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/nutrition` | POST | `nutrition_entries` | Check before, increment after | | `/api/nutrition` | GET | - | No check | | `/api/nutrition/{id}` | PUT | - | No check | | `/api/nutrition/{id}` | DELETE | - | No check | --- ### Activity Router (`/api/activity`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/activity` | POST | `activity_entries` | Check before, increment after | | `/api/activity` | GET | - | No check | | `/api/activity/{id}` | PUT | - | No check | | `/api/activity/{id}` | DELETE | - | No check | --- ### Photos Router (`/api/photos`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/photos/upload` | POST | `photos` | Check before, increment after | | `/api/photos` | GET | - | No check | | `/api/photos/{id}` | DELETE | - | No check (deleting is allowed) | --- ### Insights Router (`/api/insights`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/insights/run/{slug}` | POST | `ai_calls` | Check before, increment after | | `/api/insights/pipeline` | POST | `ai_pipeline` (boolean) | Check before (no increment for boolean) | | `/api/insights` | GET | - | No check | | `/api/insights/{id}` | GET | - | No check | **Rationale:** - `ai_calls` = count-based, monthly reset - `ai_pipeline` = boolean (enabled/disabled), no usage tracking --- ### Export Router (`/api/export`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/export/csv` | GET | `data_export` | Check before, increment after | | `/api/export/json` | GET | `data_export` | Check before, increment after | | `/api/export/zip` | GET | `data_export` | Check before, increment after | **Rationale:** Ein Feature für alle 3 Export-Typen (konsolidiert). --- ### Import Router (`/api/import`) | Endpoint | Method | Feature | Action | |----------|--------|---------|--------| | `/api/nutrition/import/fddb` | POST | `data_import` | Check before, increment after | | `/api/activity/import/csv` | POST | `data_import` | Check before, increment after | | `/api/import/zip` | POST | `data_import` | Check before, increment after | **Rationale:** Ein Feature für alle Import-Typen. --- ## Implementation Pattern (Phase 2: Non-Blocking Logging) ### Pattern für count-based Features ```python from auth import require_auth, check_feature_access, increment_feature_usage import logging logger = logging.getLogger(__name__) @router.post("/api/weight") def create_weight(data: dict, session: dict = Depends(require_auth)): profile_id = session['profile_id'] # Phase 2: Check access (log only, don't block) access = check_feature_access(profile_id, 'weight_entries') if not access['allowed']: logger.warning( f"[FEATURE-LIMIT] User {profile_id} would be blocked: " f"weight_entries limit_exceeded ({access['used']}/{access['limit']})" ) # NOTE: Phase 2 does NOT raise HTTPException - just logs! # Actual logic # ... create weight entry ... # Phase 2: Increment usage (even if limit would be exceeded) increment_feature_usage(profile_id, 'weight_entries') return {"ok": True, "id": entry_id} ``` ### Pattern für boolean Features ```python @router.post("/api/insights/pipeline") def run_pipeline(session: dict = Depends(require_auth)): profile_id = session['profile_id'] # Phase 2: Check access (log only) access = check_feature_access(profile_id, 'ai_pipeline') if not access['allowed']: logger.warning( f"[FEATURE-LIMIT] User {profile_id} would be blocked: " f"ai_pipeline disabled" ) # NOTE: Phase 2 does NOT raise HTTPException! # Actual logic # ... run pipeline ... # No increment for boolean features return {"ok": True} ``` --- ## Phase 3: Frontend Display (ohne Gates) ### Usage-Counter anzeigen ```jsx // Example: WeightPage.jsx import { useEffect, useState } from 'react' import api from '../utils/api' function WeightPage() { const [usage, setUsage] = useState(null) useEffect(() => { // Fetch usage info api.get('/api/features/weight_entries/check-access') .then(res => setUsage(res)) }, []) return (

Gewicht

{/* Phase 3: Display usage (non-blocking) */} {usage && usage.limit !== null && (
{usage.used} / {usage.limit} Einträge {usage.remaining !== null && usage.remaining < 5 && ( Nur noch {usage.remaining} Einträge verfügbar )}
)} {/* Button is NOT disabled in Phase 3 */}
) } ``` --- ## Phase 4: Enforcement aktivieren (opt-in) ### Feature-Flag System ```python # In app_settings table INSERT INTO app_settings (key, value, description) VALUES ('feature_enforcement_enabled', 'false', 'Enable/disable feature limit enforcement'); ``` ### Modified Pattern (mit Enforcement) ```python def create_weight(data: dict, session: dict = Depends(require_auth)): profile_id = session['profile_id'] # Check if enforcement is enabled enforcement_enabled = get_app_setting('feature_enforcement_enabled', False) # Check access access = check_feature_access(profile_id, 'weight_entries') if not access['allowed']: if enforcement_enabled: # Phase 4: BLOCK raise HTTPException( status_code=429, detail=f"Limit erreicht: {access['used']}/{access['limit']} Gewichtseinträge. Upgrade für mehr." ) else: # Phase 2/3: LOG ONLY logger.warning( f"[FEATURE-LIMIT] User {profile_id} would be blocked: " f"weight_entries limit_exceeded ({access['used']}/{access['limit']})" ) # Actual logic # ... ``` --- ## Rollout-Strategie ### Phase 2: Log-Only (1-2 Wochen) - Alle Checks implementiert - Nur Logging, keine Blocks - **Monitoring**: Wie oft würde blockiert? - **Analyse**: Gibt es falsche Limits? ### Phase 3: Display-Only (1 Woche) - Frontend zeigt Usage an - Buttons NICHT disabled - **User-Feedback**: Ist Usage-Anzeige klar? - **Testing**: Funktioniert Counter korrekt? ### Phase 4: Enforcement (schrittweise) 1. Admin-Account testen (enforcement=true nur für Admin) 2. Test-User (1-2 Accounts) 3. Rollout an alle (feature_enforcement_enabled=true) ### Rollback-Plan - `UPDATE app_settings SET value='false' WHERE key='feature_enforcement_enabled'` - Sofortiger Rollback ohne Code-Deploy --- ## Testing-Checklist ### Unit-Tests (Backend) - [ ] `check_feature_access()` mit allen Hierarchien - [ ] `increment_feature_usage()` mit Reset-Logik - [ ] Count-based Features (limit erreicht) - [ ] Boolean Features (enabled/disabled) - [ ] Monthly reset funktioniert ### Integration-Tests - [ ] POST weight-entry bis Limit erreicht - [ ] Limit wird korrekt in Response angezeigt - [ ] Reset nach Monatswechsel - [ ] User-Override überschreibt Tier-Limit - [ ] Access-Grant überschreibt Base-Tier ### Frontend-Tests - [ ] Usage-Counter aktualisiert nach Create - [ ] Warning bei < 5 remaining - [ ] Unlimited zeigt "∞" - [ ] Disabled-Features zeigen Upgrade-Hinweis --- **Letzte Aktualisierung:** 20. März 2026 **Autor:** Lars Stommer + Claude Opus 4.6