shinkan-jinkendo/.claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md
Lars b68185842e
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m40s
Enhance Club Feature Consumption Logic and Update Versioning
- Introduced the `consume_club_feature_with_usage` function to standardize feature consumption across endpoints, improving code reusability and clarity.
- Implemented `merge_feature_usage_into_response` to embed feature usage data in API responses, streamlining frontend integration.
- Updated various backend routers to utilize the new consumption logic, ensuring consistent feature usage tracking during AI-related actions.
- Enhanced tests to validate the new consumption and logging behavior.
- Incremented application version to 0.8.199 and updated module version for 'club_features' to 1.6.0 to reflect these changes.
2026-06-07 10:32:49 +02:00

479 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Vereins-Membership & Feature-System Shinkan v1
**Status:** Konzept + M1M3 teilweise produktiv (siehe Entscheidungs-Doc §2)
**Stand:** 2026-06-06
**Bezüge:** Schwesterprojekt Mitai (`v9c_subscription_system.sql`, `FEATURE_ENFORCEMENT.md`), `CAPABILITY_CATALOG.v1.md`, `MULTI_TENANCY_RBAC_ARCHITECTURE.md` §4.6, `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, **`MEMBERSHIP_RBAC_DECISIONS_2026-06.md`**
---
## 1. Zweck
Shinkan verkauft und limitiert **nicht Einzelpersonen** (wie Mitai), sondern **Vereine**. Dieses Dokument definiert:
- das **Feature-Registry**-Muster (limitierbare Funktionen),
- das **Vereins-Abo** (`club_plans`, `club_subscriptions`),
- **Kontingente** und Enforcement,
- die **Abbildung von Mitai** und **Vermeidung von Refactoring-Schulden**.
Capabilities (Rollen: *darf ich die Funktion?*) → `CAPABILITY_CATALOG.v1.md`.
---
## 2. Grundprinzip: Zwei Achsen
```mermaid
flowchart TB
subgraph cap [Achse 1 — Capabilities]
CR[club_role_capability_grants]
PR[portal_role_capability_grants]
end
subgraph feat [Achse 2 — Features / Kontingente]
FP[club_plans]
FPL[club_plan_limits]
FS[club_subscriptions]
FU[club_feature_usage]
end
subgraph gov [Achse 3 — Governance]
GV[visibility / club_id / created_by]
end
REQ[HTTP Request] --> ACCT[Account-Lifecycle]
ACCT --> cap
cap --> gov
gov --> feat
feat --> EXEC[Ausführung + increment]
```
| Frage | System | Subjekt |
|-------|--------|---------|
| Darf Trainer X KI nutzen? | Capability `exercises.ai.suggest` | `profile_id` + `club_role` |
| Wie viele KI-Aufrufe hat Verein Y? | Feature `ai_calls` | **`club_id`** |
| Darf ich diese Übung ändern? | Governance | Objekt + Mitgliedschaft |
**Beide Achsen müssen erfüllt sein** (AND), außer dokumentierte Plattform-Ausnahmen.
---
## 3. Mitai-Mapping (was übernehmen, was nicht)
### 3.1 Übernehmen (Pattern)
| Mitai (Person) | Shinkan (Verein) | Anmerkung |
|----------------|------------------|-----------|
| `features` (TEXT-PK, Registry) | `features` (`app='shinkan'`) | Gemeinsames Muster, ggf. später Jinkendo-weit |
| `tiers` | `club_plans` | Produktdefinition |
| `tier_limits` | `club_plan_limits` | Matrix Plan × Feature |
| `user_feature_restrictions` | `club_feature_overrides` | Admin-Override pro Verein |
| `user_feature_usage` | `club_feature_usage` | Verbrauch pro Verein |
| `access_grants` | `club_access_grants` | Trial, Promo, manuelle Freischaltung |
| `check_feature_access()` | `check_club_feature_access()` | Subjekt `club_id` |
| `increment_feature_usage()` | `increment_club_feature_usage()` | Nur bei INSERT / KI-Call |
| 4-Phasen-Rollout | identisch | Log → UI → Hard-Block |
| `GET /api/features/usage` | `GET /api/clubs/{id}/entitlements` | siehe Capability-Doc §7 |
### 3.2 Nicht übernehmen
| Mitai | Shinkan-Grund |
|-------|---------------|
| `profiles.tier` als Haupt-Abo | Verein zahlt, nicht Einzeltrainer |
| `subscriptions` (Shinkan `001`, INT-Features) | Ungenutzt, Schema-Drift |
| `get_effective_tier(profile_id)` für Shinkan-Limits | Ersetzen durch `get_effective_club_plan(club_id)` |
| Profil-zentrierte Enforcement-Hooks allein | Primär `club_id`; Profil nur für Attribution |
### 3.3 Parallelität Jinkendo-Familie (später)
`CENTRAL_SUBSCRIPTION_SYSTEM.md` (Mitai): zentrales Personen-Abo über Apps.
**Zielbild ohne Refactoring:**
```
features.enforcement_subject ∈ { 'club', 'profile', 'portal' }
effektives_limit(feature) = merge(
club_plan_limit(club_id, feature), # Shinkan-Hauptquelle
profile_grant_limit(profile_id, feature) # optional Jinkendo-Bonus
)
```
Merge-Regel (Vorschlag): **Maximum** der erlaubten Kontingente, boolean = OR. Details vor Stripe festlegen.
---
## 4. Ist-Zustand Shinkan (Drift — zuerst bereinigen)
| Artefakt | Problem |
|----------|---------|
| `backend/migrations/001_auth_membership.sql` | `features.id SERIAL`, `tier_limits.tier VARCHAR` |
| `backend/auth.py` `check_feature_access()` | Erwartet Mitai-v9c-Schema (`features.id TEXT`, `tier_id`, `limit_type`, …) |
| Kein Router | Ruft `check_feature_access` auf |
| `profiles.tier` | Existiert, ohne Shinkan-Enforcement |
**Pflicht vor Phase 3 (Enforcement):** Migration `0XX_club_features_v1.sql` — v9c-kompatibles Feature-Schema + Vereins-Tabellen; alte `001`-Feature-Zeilen migrieren oder deprecaten.
---
## 5. Ziel-Schema (v1)
### 5.1 Feature-Registry (app-weit, Mitai-kompatibel)
```sql
-- Konzept — Implementierung als nummerierte Migration
CREATE TABLE features (
id TEXT PRIMARY KEY, -- z.B. 'ai_calls'
app TEXT NOT NULL DEFAULT 'shinkan',
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL, -- 'content'|'planning'|'ai'|'org'|'integration'|'platform'
limit_type TEXT NOT NULL DEFAULT 'count', -- 'count' | 'boolean'
reset_period TEXT NOT NULL DEFAULT 'never', -- 'never' | 'daily' | 'monthly'
default_limit INTEGER, -- NULL=∞, 0=aus
enforcement_subject TEXT NOT NULL DEFAULT 'club', -- 'club'|'profile'|'portal'
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
### 5.2 Vereins-Produkte & Abo
```sql
CREATE TABLE club_plans (
id TEXT PRIMARY KEY, -- 'free', 'verein_starter', 'verein_pro'
name TEXT NOT NULL,
description TEXT,
price_monthly_cents INTEGER,
price_yearly_cents INTEGER,
stripe_price_id_monthly TEXT,
stripe_price_id_yearly TEXT,
active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE club_subscriptions (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT NOT NULL REFERENCES club_plans(id),
status TEXT NOT NULL DEFAULT 'active', -- active|trial|past_due|cancelled
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ends_at TIMESTAMPTZ,
trial_ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (club_id) -- ein aktiver Plan pro Verein (v1)
);
CREATE TABLE club_plan_limits (
id SERIAL PRIMARY KEY,
plan_id TEXT NOT NULL REFERENCES club_plans(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER, -- NULL=∞, 0=deaktiviert
UNIQUE (plan_id, feature_id)
);
```
### 5.3 Overrides, Grants, Verbrauch
```sql
CREATE TABLE club_feature_overrides (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER NOT NULL,
reason TEXT,
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (club_id, feature_id)
);
CREATE TABLE club_access_grants (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT REFERENCES club_plans(id),
feature_id TEXT REFERENCES features(id), -- optional Einzel-Feature
grant_limit INTEGER,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
reason TEXT,
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL
);
CREATE TABLE club_feature_usage (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
usage_count INTEGER NOT NULL DEFAULT 0,
reset_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
UNIQUE (club_id, feature_id)
);
-- Optional: Attribution / Fairness / Audit
CREATE TABLE club_feature_usage_events (
id BIGSERIAL PRIMARY KEY,
club_id INT NOT NULL,
feature_id TEXT NOT NULL,
profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
action TEXT NOT NULL, -- 'ai_suggest', 'exercise_create', ...
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### 5.4 Capabilities (Rollen — Kurzreferenz)
Siehe `CAPABILITY_CATALOG.v1.md` für IDs. Tabellen:
```sql
CREATE TABLE capabilities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
domain TEXT NOT NULL,
min_account_state TEXT NOT NULL DEFAULT 'active_member',
linked_feature_id TEXT REFERENCES features(id), -- optional Kontingent
active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE club_role_capability_grants (
role_code TEXT NOT NULL, -- club_admin, trainer, ...
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (role_code, capability_id)
);
CREATE TABLE portal_role_capability_grants (
portal_role TEXT NOT NULL, -- admin, superadmin
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (portal_role, capability_id)
);
```
---
## 6. Shinkan Feature-Katalog (Seed v1)
Übernahme aus `001_auth_membership.sql` + Ist-Endpoints, angereichert:
| feature_id | category | limit_type | reset_period | enforcement_subject | Default Free | Beschreibung |
|------------|----------|------------|--------------|---------------------|--------------|--------------|
| `exercises` | content | count | never | club | 100 | Anzahl Übungen im Verein (Bestand) |
| `exercise_media` | content | count | monthly | club | 20 | Medien-Uploads / Monat |
| `training_units` | planning | count | monthly | club | 40 | Geplante/durchgeführte Einheiten |
| `training_programs` | planning | count | never | club | 5 | Module + Rahmenprogramme (kombiniert v1) |
| `training_groups` | org | count | never | club | 10 | Trainingsgruppen |
| `active_members` | org | count | never | club | 25 | Aktive Mitglieder |
| `ai_calls` | ai | count | monthly | club | 0 | KI-Aufrufe (Suggest, Regenerate, Planung) |
| `ai_pipeline` | ai | boolean | never | club | 0 | Erweiterte KI-Pipelines (Batch, später) |
| `wiki_import` | integration | boolean | never | portal | 0 | MediaWiki-Import (Superadmin) |
| `data_export` | integration | boolean | never | club | 0 | Export-Funktionen (wenn eingeführt) |
**Hinweis:** Free-Defaults sind Produktentscheidung — Tabelle dient Implementierung.
### 6.1 Beispiel-Pläne (Seed)
| plan_id | ai_calls/Monat | exercises | active_members |
|---------|----------------|-----------|----------------|
| `free` | 0 | 100 | 25 |
| `verein_starter` | 30 | 500 | 80 |
| `verein_pro` | 200 | NULL (∞) | NULL |
| `pilot` | 100 | NULL | NULL |
Jeder Verein erhält bei Anlage durch Superadmin initial `club_subscriptions.plan_id = 'free'` (oder `pilot`).
---
## 7. Auflösungslogik
### 7.1 Effektiver Vereinsplan
```python
def get_effective_club_plan(cur, club_id: int) -> str:
"""
1. Aktiver club_access_grants mit plan_id (höchste Priorität, Zeitfenster)
2. club_subscriptions.status == 'active' → plan_id
3. Fallback 'free'
"""
```
### 7.2 Feature-Limit (analog Mitai `check_feature_access`)
```python
def check_club_feature_access(
cur,
club_id: int,
feature_id: str,
*,
profile_id: int | None = None, # nur für Logging / optionale Profil-Boni später
) -> dict:
"""
Priorität:
1. club_feature_overrides (club_id, feature_id)
2. club_plan_limits für get_effective_club_plan(club_id)
3. features.default_limit
Auswertung:
- limit_type boolean: limit_value == 1
- limit_type count: used < limit (club_feature_usage, reset beachten)
Returns: { allowed, limit, used, remaining, reason, reset_at }
"""
```
### 7.3 Vollständige Request-Kette
```
1. require_auth
2. assert_account_state(min_state) # unverified / verified_pending_club / active_member
3. get_tenant_context
4. assert_capability(tenant, cap_id) # Rollen-Achse
5. assert_content_governance(...) # nur bei Objekt-Endpoints
6. check_club_feature_access(club_id, feature_id)
7. … Business-Logik …
8. consume_club_feature_with_usage(…) + merge_feature_usage_into_response(payload, usage)
# Standard: zählen, JSON-Log phase=consume, feature_usage in Response
9. optional: club_feature_usage_events (profile_id, action)
```
**Response-Standard (alle Consume-Endpoints):** JSON-Feld `feature_usage` — Map `feature_id → { allowed, used, limit, remaining, reason, … }` wie `GET /me/entitlements`. Frontend: `request()` synchronisiert Entitlements automatisch (`featureUsageSync.js`); UI-Komponenten brauchen keinen Einzelcode.
### 7.4 Wer zählt als Verbrauch?
| Aktion | increment | Subjekt |
|--------|-----------|---------|
| `POST /exercises` (neu) | `exercises` | `club_id` des Objekts oder `effective_club_id` |
| Medien-Upload | `exercise_media` | Verein des Mediums |
| KI Suggest/Regenerate | `ai_calls` | `effective_club_id` |
| Mitglied hinzufügen | `active_members` | Ziel-`club_id` |
| Trainingsgruppe anlegen | `training_groups` | `club_id` |
**Mitai-Regel:** Counter **nicht** bei UPDATE/DELETE erhöhen.
---
## 8. API-Oberfläche
### 8.1 Nutzer / Vereinsadmin
```
GET /api/clubs/{club_id}/entitlements
```
Kombiniert Capabilities + Feature-Kontingente (siehe `CAPABILITY_CATALOG.v1.md` §7.1).
```
GET /api/me/entitlements?club_id=12
```
Bequemer Alias für aktiven Verein.
### 8.2 Superadmin / Plattform
| Endpoint | Zweck |
|----------|-------|
| `GET/PUT /api/admin/club-plans` | Plan-CRUD |
| `GET/PUT /api/admin/club-plan-limits` | Matrix |
| `GET/PUT /api/admin/clubs/{id}/subscription` | Verein-Abo |
| `GET/PUT /api/admin/clubs/{id}/feature-overrides` | Sonderkontingente |
| `POST /api/admin/clubs/{id}/access-grants` | Trial/Promo |
Vorbild UI: Mitai `AdminTierLimitsPage.jsx`, `AdminUserRestrictionsPage.jsx` → Vereins-Kontext.
### 8.3 Geplant: Vereinsgründung
```
POST /api/club-creation-requests # Nutzer (verified_pending_club)
GET /api/admin/club-creation-requests
POST /api/admin/club-creation-requests/{id}/approve # legt club + subscription an
```
---
## 9. Vier-Phasen-Rollout (aus Mitai)
| Phase | Shinkan-Aktivität | Nutzer sichtbar? |
|-------|-------------------|------------------|
| **0** | Schema-Migration, Seed `features` + `club_plans`, Drift `001` bereinigen | Nein |
| **1** | Account-Gates + Capability-Grants (ohne Limits) | Onboarding-Hinweise |
| **2** | `check_club_feature_access`**nur JSON-Log** (`feature_logger` analog Mitai) | Nein |
| **3** | `GET …/entitlements` + UsageBadge im UI | Ja (Kontingent-Anzeige) |
| **4** | HTTP 403 bei Limit + `increment` | Ja (Hard-Block) |
**Reihenfolge innerhalb Phase 4:** zuerst `ai_calls`, dann `exercise_media`, dann Bestands-Limits (`exercises`, `active_members`).
---
## 10. CI / Test-Isolation (Betrieb)
Unabhängig vom Membership-System — **Pflicht** wegen Prod-Vorfälle (`access_layer_it_*@test.local`):
| Regel | Umsetzung |
|-------|-----------|
| Integrationstests nie gegen Prod-DB | Eigene Test-DB oder Job-Postgres in Gitea |
| `ENVIRONMENT=production` + `ALLOW_INTEGRATION_TESTS` | Default `0`, Tests abbrechen |
| Test-Accounts | E-Mail `@test.local` oder `profiles.is_test_account` |
| Cleanup | Fixture-`finally` + Nightly-Job löscht Leichen |
`.gitea/workflows/test.yml`: pytest-backend gegen Deploy-DB **ersetzen** durch isolierte DB (eigenes Epic, parallel zu Membership).
---
## 11. Implementierungs-Roadmap (gesamt)
| Schritt | Deliverable | Membership-relevant |
|---------|-------------|-------------------|
| M0 | CI-Isolation + Prod-Cleanup-Runbook | Nein |
| M1 | Migration Feature-Schema v9c + `club_plans`/`club_subscriptions` (leer nutzbar) | **Ja** |
| M2 | `check_club_feature_access` + Seed Pläne | **Ja** |
| M3 | Account-Lifecycle + Capability-Grants | Capabilities |
| M4 | `GET /me/entitlements` | **Ja** |
| M5 | Enforcement `ai_calls` (Phase 4) | **Ja** |
| M6 | Admin Plan-Matrix UI | **Ja** |
| M7 | `club_creation_requests` | Prozess |
| M8 | Stripe / Rechnung | Später |
**Nach Produktentscheidungen 2026-06-06** (Details `MEMBERSHIP_RBAC_DECISIONS_2026-06.md` §4):
| Phase | Paket | Priorität |
|-------|--------|-----------|
| A | Onboarding-Gates vollständig (`verified_pending_club`) | **Als Nächstes** |
| B | M7 Vereinsgründung beantragen | hoch |
| C | M5 Hard-Block `ai_calls` | danach |
| D | M6 Superadmin-UI | danach |
| E | Systemrolle `co_trainer` + Frontend-Entitlements | v1 Rollen |
| F | Trainer-Member-Budgets (v2) | später |
---
## 12. Offene Produktentscheidungen
Vor M6 festlegen:
1. **Zählen `active_members`:** alle Mitglieder oder nur Rollen mit Planungsrecht?
2. **Soft-Limit vs. Hard-Stop:** Warnung bei 80 % oder sofort 403?
3. **Pilotverein:** eigener Plan `pilot` mit hohen Limits?
4. **KI-Fairness:** nur Vereinslimit oder zusätzlich Max pro Trainer/Monat?
5. **Offizielle Inhalte:** für `verified_pending_club` sichtbar oder gesperrt? → **entschieden: gesperrt** (`MEMBERSHIP_RBAC_DECISIONS_2026-06.md` §1.1)
6. **Portal `admin` vs. `superadmin`:** Wer darf Vereine anlegen? (Ziel: nur `superadmin` für Freigabe)
---
## 13. Referenzen
| Pfad | Inhalt |
|------|--------|
| `c:/dev/mitai-jinkendo/backend/migrations/v9c_subscription_system.sql` | Mitai-Schema-Vorlage |
| `c:/dev/mitai-jinkendo/.claude/docs/architecture/FEATURE_ENFORCEMENT.md` | 4-Phasen-Modell |
| `c:/dev/mitai-jinkendo/.claude/docs/technical/MEMBERSHIP_SYSTEM.md` | Mitai-Hauptdoku |
| `c:/dev/mitai-jinkendo/.claude/docs/technical/CENTRAL_SUBSCRIPTION_SYSTEM.md` | Jinkendo-Familie später |
| `CAPABILITY_CATALOG.v1.md` | Rollen & Capabilities |
| `MULTI_TENANCY_RBAC_ARCHITECTURE.md` §4.6 | Ursprüngliches Vereinsabo-Zielbild |
| `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Stufe E/F |
---
**Changelog**
- 2026-06-06: v1 — Mitai-Mapping, Ziel-Schema, Feature-Seed, Auflösungslogik, Rollout.