# Billing & Modul-Abonnements

eforms.cloud verwendet ein **per-Modul-Abonnement-System**. Jedes Modul (eForm, eEvent, eQuiz, ePoll) kann unabhaengig abonniert werden, jeweils in einer von 4 Stufen. Ohne aktives Abonnement oder gueltige Lizenz ist ein Modul nicht zugaenglich.

## Betriebsmodi

| Modus | Beschreibung | Modul-Aktivierung |
|-------|-------------|-------------------|
| **SaaS** | Gehostete Version | Stripe-Webhooks schreiben direkt in die Datenbank |
| **Self-Hosted** | Eigene Installation | Pro-Modul-Lizenzschluessel (signierte JWTs) |

Der Modus wird ueber die Umgebungsvariable `LICENSE_MODE` gesteuert (`saas` oder `self_hosted`).

---

## Module

| Modul | Interner Name | Beschreibung |
|-------|--------------|--------------|
| **eForm** | `forms` | Formular-Erstellung, Block-Editor, Template-Editor, Submissions |
| **eEvent** | `events` | Event-Management, Ticketing, Check-In |
| **eQuiz** | `quizzes` | Quizze und Umfragen |
| **ePoll** | `epoll` | Terminabstimmung und Terminplanung |

## Stufen (Tiers)

Jedes Modul hat 4 Abo-Stufen mit aufsteigenden Limits:

| Stufe | Rang | Beschreibung |
|-------|------|--------------|
| **Starter** | 1 | Einstieg mit Basis-Limits |
| **Professional** | 2 | Erweiterte Limits fuer kleine Teams |
| **Business** | 3 | Groessere Limits fuer Organisationen |
| **Enterprise** | 4 | Unbegrenzt |

### Modul-Limits pro Stufe

| Modul | Starter | Professional | Business | Enterprise |
|-------|---------|-------------|----------|-----------|
| eForm | 1 Template | 5 Templates | 15 Templates | unbegrenzt |
| eEvent | 3 Events | 10 Events | 50 Events | unbegrenzt |
| eQuiz | 3 Quizze | 10 Quizze | 50 Quizze | unbegrenzt |
| ePoll | 3 Polls | 15 Polls | 50 Polls | unbegrenzt |

---

## Cross-Cutting Features

Einige Features sind nicht an ein einzelnes Modul gebunden, sondern richten sich nach der **hoechsten aktiven Stufe** ueber alle Module einer Organisation.

| Feature | Mindest-Stufe | Beschreibung |
|---------|--------------|--------------|
| `bi_api` | Professional | BI-API fuer externe Analytics-Tools |
| `audit_log` | Professional | Audit-Logging aller Aktionen |
| `email_notifications` | Business | E-Mail-Benachrichtigungen bei Submissions |
| `scheduled_reports` | Business | Automatische E-Mail-Berichte |
| `white_label` | Business | White-Label-Branding |
| `sso` | Enterprise | Single Sign-On (LDAP/SAML/OIDC) |
| `multi_tenancy` | Enterprise | Multi-Mandanten-Faehigkeit |

**Beispiel:** Eine Organisation hat eForm (Starter) und eEvent (Business). Die hoechste Stufe ist *Business*, daher sind `bi_api`, `audit_log`, `email_notifications`, `scheduled_reports` und `white_label` verfuegbar — aber nicht `sso` oder `multi_tenancy`.

---

## Datenbank-Schema

### `module_subscriptions`-Tabelle

| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| `id` | bigint | Primaerschluessel |
| `organization_id` | FK | Zugehoerigkeit zur Organisation |
| `module` | string | `forms`, `events`, `quizzes`, `epoll` |
| `tier` | string | `starter`, `professional`, `business`, `enterprise` |
| `source` | string | `stripe` (SaaS) oder `license` (Self-Hosted) |
| `stripe_subscription_id` | string? | Stripe-Subscription-ID (SaaS) |
| `stripe_customer_id` | string? | Stripe-Customer-ID (SaaS) |
| `stripe_price_id` | string? | Stripe-Price-ID (SaaS) |
| `license_key` | text? | JWT-Lizenzschluessel (Self-Hosted) |
| `status` | string | `active`, `trialing`, `past_due`, `canceled` |
| `current_period_end` | timestamp? | Ende der aktuellen Abrechnungsperiode |
| `canceled_at` | timestamp? | Kuendigungszeitpunkt |

**Constraint:** `UNIQUE(organization_id, module)` — pro Organisation maximal ein Abonnement pro Modul.

---

## PlanService

Zentrale Klasse fuer Abonnement-Abfragen. Cached pro Organisation fuer 1 Stunde.

### Methoden

| Methode | Rueckgabe | Beschreibung |
|---------|-----------|--------------|
| `hasModule(string)` | `bool` | Prueft ob die aktuelle Organisation ein aktives Abo fuer das Modul hat |
| `moduleTier(string)` | `?string` | Gibt die Stufe fuer ein Modul zurueck (oder null) |
| `hasFeature(string)` | `bool` | Prueft Modul-Zugang oder Cross-Cutting-Feature |
| `moduleLimit(string, string)` | `mixed` | Modul-spezifisches Limit lesen (z.B. `max_templates`) |
| `templateLimit()` | `?int` | Max. aktive Templates. `null` = unbegrenzt, `0` = kein forms-Abo |
| `canActivateTemplate(?int)` | `bool` | Kann ein weiteres Template aktiviert werden? |
| `activeTemplateCount()` | `int` | Anzahl aktuell aktiver Templates |
| `highestTier()` | `?string` | Hoechste Stufe ueber alle Module |
| `subscriptionInfo()` | `array` | Vollstaendige Abo-Infos fuer Frontend |
| `clearCache()` | `void` | Cache fuer aktuelle Organisation invalidieren |
| `clearCacheForOrganization(int)` | `void` | Cache fuer spezifische Organisation invalidieren |

### Feature-Check-Logik

`hasFeature()` unterscheidet automatisch zwischen Modul-Features und Cross-Cutting-Features:

```
hasFeature('events')  → hasModule('events')         → prueft module_subscriptions
hasFeature('bi_api')  → highestTier() >= professional → prueft hoechste Stufe
hasFeature('sso')     → highestTier() >= enterprise   → prueft hoechste Stufe
```

---

## CheckPlanFeature Middleware

Route-Middleware fuer Feature-Gating. Funktioniert sowohl fuer Module als auch Cross-Cutting-Features:

```php
// Modul-Zugang pruefen
Route::middleware('plan.feature:forms')->group(function () { ... });
Route::middleware('plan.feature:events')->group(function () { ... });

// Cross-Cutting-Feature pruefen
Route::middleware('plan.feature:bi_api')->group(function () { ... });
Route::middleware('plan.feature:sso')->group(function () { ... });
```

Bei fehlendem Zugang: HTTP 403 mit `required_feature` und `highest_tier`.

### Geschuetzte Features

| Feature-Key | Geschuetzte Routen |
|-------------|-------------------|
| `forms` | `/admin/form-blocks`, `/admin/form-templates`, `/meldungen`, `/dashboard` |
| `events` | `/admin/events/*` |
| `bi_api` | `/bi/*` (BI-API-Endpunkte) |
| `audit_log` | `/admin/audit-logs` |
| `scheduled_reports` | `/admin/reports` |
| `multi_tenancy` | `/admin/organization`, `/admin/organizations` |
| `sso` | `/admin/sso-providers` |
| `email_notifications` | `/admin/email-notifications` |

---

## Stripe-Integration (SaaS-Modus)

Stripe verwaltet Abonnements im SaaS-Betrieb. Jedes Stripe-Produkt/Preis traegt Metadata, die Modul und Stufe definieren.

### Konfiguration

```env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
```

### Stripe Metadata

Im Stripe-Dashboard auf Produkt oder Preis:

```
metadata.module = "forms"         // forms, events, quizzes, epoll
metadata.tier   = "professional"  // starter, professional, business, enterprise
```

### StripeService

| Methode | Beschreibung |
|---------|--------------|
| `isEnabled()` | Prueft ob Stripe konfiguriert ist |
| `constructWebhookEvent(payload, sigHeader)` | Stripe-Signatur validieren, Event parsen |
| `extractModuleAndTier(subscription)` | Modul + Stufe aus Metadata extrahieren |

### Webhook-Flow

```mermaid
sequenceDiagram
    participant Stripe
    participant WH as /api/webhooks/stripe
    participant SS as StripeService
    participant DB as module_subscriptions
    participant PS as PlanService

    Stripe->>WH: POST (Stripe-Signature Header)
    WH->>SS: constructWebhookEvent()
    SS-->>WH: Stripe\Event
    WH->>SS: extractModuleAndTier(subscription)
    SS-->>WH: {module, tier}
    WH->>DB: ModuleSubscription::updateOrCreate()
    WH->>PS: clearCacheForOrganization()
```

### Webhook-Events

| Event | Aktion |
|-------|--------|
| `customer.subscription.created` | ModuleSubscription erstellen (source='stripe') |
| `customer.subscription.updated` | Stufe/Status aktualisieren (Upgrade/Downgrade) |
| `customer.subscription.deleted` | Status auf 'canceled' setzen |
| `invoice.payment_failed` | Loggen (keine sofortige Deaktivierung) |

### Organisation-Zuordnung

Die Organisation wird ueber `stripe_customer_id` auf der `organizations`-Tabelle aufgeloest. Beim Erstellen eines Stripe-Customers muss die `stripe_customer_id` in der Organisation gespeichert werden.

---

## Lizenzschluessel (Self-Hosted-Modus)

Fuer Self-Hosted-Installationen werden Module ueber signierte JWT-Lizenzschluessel aktiviert. Pro Modul wird ein eigener Schluessel benoetigt.

### Konfiguration

```env
LICENSE_MODE=self_hosted
LICENSE_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIB..."
LICENSE_VALIDATION_URL=https://license.eforms.cloud/api/validate
LICENSE_PHONE_HOME_INTERVAL=24
```

### JWT-Payload-Struktur

```json
{
  "iss": "eforms.cloud",
  "sub": "license",
  "module": "forms",
  "tier": "professional",
  "org_id": "*",
  "license_id": "lic_abc123",
  "exp": 1735689600,
  "iat": 1704153600
}
```

| Claim | Beschreibung |
|-------|--------------|
| `module` | Freigeschaltetes Modul |
| `tier` | Abo-Stufe |
| `org_id` | `*` = beliebige Organisation, oder spezifische ID |
| `license_id` | Eindeutige Lizenz-ID (fuer Widerruf) |
| `exp` | Ablauf-Zeitpunkt (Unix-Timestamp) |

Die Signatur ist **RS256** (RSA + SHA-256). Der Public Key ist in der Instanz hinterlegt, der Private Key liegt beim Lizenz-Server.

### LicenseService

| Methode | Beschreibung |
|---------|--------------|
| `validateLicense(jwt)` | JWT-Signatur und Claims pruefen. Gibt Claims oder `false` zurueck. |
| `activateLicense(jwt, orgId)` | Lizenz validieren und ModuleSubscription erstellen (source='license') |
| `deactivateLicense(module, orgId)` | Modul-Lizenz deaktivieren |
| `phoneHome()` | Alle aktiven Lizenzen gegen Remote-Server pruefen, widerrufene deaktivieren |

### Lizenz-Verwaltung (Admin-API)

| Endpunkt | Methode | Beschreibung |
|----------|---------|--------------|
| `/api/admin/licenses` | GET | Alle aktiven Lizenzen der Organisation |
| `/api/admin/licenses/activate` | POST | Lizenzschluessel aktivieren (`license_key` im Body) |
| `/api/admin/licenses/{module}` | DELETE | Modul-Lizenz deaktivieren |

### Phone-Home (Lizenz-Widerruf)

Optionaler Mechanismus zum Widerrufen von Lizenzen:

1. Scheduler laeuft alle X Stunden (konfigurierbar via `LICENSE_PHONE_HOME_INTERVAL`)
2. Sammelt alle aktiven `license_id`s
3. Sendet POST an `LICENSE_VALIDATION_URL` mit `{ "license_ids": ["lic_abc", ...] }`
4. Server antwortet mit `{ "revoked": ["lic_abc"] }`
5. Widerrufene Lizenzen werden auf `status='canceled'` gesetzt

Wenn kein `LICENSE_VALIDATION_URL` konfiguriert ist, erfolgt nur Offline-Validierung via JWT-Signatur + Ablaufdatum.

---

## Upgrade/Downgrade

### Upgrade (Stripe)

1. Stripe sendet `customer.subscription.updated` mit neuer Price-Metadata
2. `StripeWebhookController` extrahiert neues `tier`
3. `ModuleSubscription` wird aktualisiert
4. Cache wird invalidiert
5. Neue Limits sind sofort verfuegbar

### Downgrade (Stripe)

1. Neues `tier` wird gesetzt
2. Modul-Limits greifen sofort
3. Bestehende Ressourcen ueber dem neuen Limit werden **nicht** automatisch deaktiviert
4. Neue Ressourcen koennen nicht erstellt werden, bis unter dem Limit

### Kuendigung (Stripe)

1. Stripe sendet `customer.subscription.deleted`
2. `ModuleSubscription.status` wird auf `canceled` gesetzt
3. Modul ist sofort nicht mehr zugaenglich

---

## Plan-Info-Endpoint

`GET /api/plan` liefert dem Frontend den aktuellen Abo-Stand:

```json
{
  "subscriptions": {
    "forms": {
      "tier": "professional",
      "status": "active",
      "limits": { "max_templates": 5 },
      "current_period_end": "2027-03-01T00:00:00+00:00"
    },
    "events": {
      "tier": "business",
      "status": "active",
      "limits": { "max_events": 50 },
      "current_period_end": "2027-03-01T00:00:00+00:00"
    }
  },
  "features": {
    "bi_api": true,
    "audit_log": true,
    "email_notifications": true,
    "scheduled_reports": true,
    "white_label": true,
    "sso": false,
    "multi_tenancy": false
  },
  "highest_tier": "business",
  "stripe_connected": true
}
```

---

## Umgebungsvariablen

### Stripe (SaaS)

| Variable | Standard | Beschreibung |
|----------|---------|--------------|
| `STRIPE_SECRET_KEY` | (leer) | Stripe Secret Key |
| `STRIPE_PUBLISHABLE_KEY` | (leer) | Stripe Publishable Key |
| `STRIPE_WEBHOOK_SECRET` | (leer) | Stripe Webhook Signing Secret |

### Lizenz (Self-Hosted)

| Variable | Standard | Beschreibung |
|----------|---------|--------------|
| `LICENSE_MODE` | `self_hosted` | Betriebsmodus: `saas` oder `self_hosted` |
| `LICENSE_PUBLIC_KEY` | (leer) | RS256 Public Key fuer JWT-Validierung |
| `LICENSE_VALIDATION_URL` | (leer) | URL fuer Phone-Home-Widerrufspruefung |
| `LICENSE_PHONE_HOME_INTERVAL` | `24` | Phone-Home-Intervall in Stunden (0 = deaktiviert) |

---

## Konfigurationsdateien

| Datei | Beschreibung |
|-------|--------------|
| `config/modules.php` | Modul-Definitionen, Stufen, Cross-Cutting-Features, Limits |
| `config/stripe.php` | Stripe API-Schluessel und Webhook-Secret |
| `config/license.php` | Lizenzmodus, Public Key, Phone-Home-Einstellungen |
