shinkan-jinkendo/.claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md

479 lines
18 KiB
Markdown
Raw Normal View History

# Vereins-Membership & Feature-System Shinkan v1
**Status:** Konzept + M1–M3 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.