# 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 (
    <div>
      <h1>Gewicht</h1>

      {/* Phase 3: Display usage (non-blocking) */}
      {usage && usage.limit !== null && (
        <div className="usage-badge">
          {usage.used} / {usage.limit} Einträge
          {usage.remaining !== null && usage.remaining < 5 && (
            <span className="warning">
              Nur noch {usage.remaining} Einträge verfügbar
            </span>
          )}
        </div>
      )}

      {/* Button is NOT disabled in Phase 3 */}
      <button onClick={createEntry}>
        Gewicht hinzufügen
      </button>
    </div>
  )
}
```

---

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