# 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 (