# Vereins-Membership & Feature-System Shinkan v1 **Status:** Konzept (Schema- und Enforcement-Zielbild vor Implementierung) **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` --- ## 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. increment_club_feature_usage(club_id, feature_id) # nur bei INSERT / KI-Execute 9. optional: log club_feature_usage_events (profile_id) ``` ### 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 | --- ## 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? (Capability-Doc) 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.