# AV-Vertrag-Generator (Art. 28 DSGVO)

Modul für eforms.cloud, das Mandanten (Auftragsverarbeiter) den Abschluss
elektronischer Auftragsverarbeitungsverträge mit ihren Endkunden
(Verantwortlichen) als Self-Service im Portal erlaubt. Stufe 2:
**Klick-Akzeptanz mit Audit**, rechtssicher nach Art. 28 Abs. 9 DSGVO.

## Voraussetzungen

| Anforderung | Detail |
|---|---|
| Plan-Tier | mindestens `business` (cross-cutting Feature `dpa_generator`) |
| Aktive Module | `portal` muss subscribed sein — der Generator lebt im ePortal |
| Member-Account | Endkunde muss als Portal-Mitglied registriert + verifiziert sein |
| Infrastruktur | Queue-Container mit Chromium (siehe `backend/Dockerfile`) und `shm_size: 1gb` |

## Setup-Schritte (Tenant)

### 1. Verarbeiter-Identität konfigurieren

Im Admin-Bereich unter **Organisation → Einstellungen** im Block
`settings.dpa_processor` als JSON oder via UI eintragen:

```json
{
  "legal_name": "Mustermann GmbH",
  "address": "Musterstr. 1, 12345 Musterstadt",
  "representative": "Erika Mustermann",
  "dpo_contact": "datenschutz@mustermann.de"
}
```

Diese Werte werden in der gerenderten PDF als `{{tenant.legal_name}}`,
`{{tenant.address}}`, `{{tenant.representative}}`, `{{tenant.dpo_contact}}`
eingesetzt. Ohne sie greifen Fallbacks auf `organizations.name` und die
postalische Adresse aus `street/zip/city`.

### 2. Vorlage anlegen

Zwei Wege:

**(A) DSK-System-Vorlage forken (empfohlen):** `/admin/dpa-templates`
öffnen → Vorlage `dsk-standard` auswählen → Button **„In Tenant kopieren"**.
Die Kopie ist editierbar.

**(B) Leere Vorlage erstellen:** Button **„Neu"** in der Listenleiste.

> **Rechtlicher Hinweis:** Die mitgelieferte DSK-Vorlage ersetzt KEINE
> anwaltliche Prüfung. eforms.cloud übernimmt keine Haftung für den Inhalt.
> Bitte vor produktivem Einsatz juristisch prüfen lassen.

### 3. Vorlage editieren

- **Vertragstext (Markdown):** Freitext mit Mustache-Platzhaltern.
- **Variablen-Schema (JSON):** Definiert die Wizard-Felder.
- **Klauseln (JSON):** Bedingte Vertragsteile (siehe „Klausel-Bedingungen").
- **Aufbewahrung (Tage):** Default 1095 = 3 Jahre (BGB §195). Pro Vorlage anpassbar.

### 4. Veröffentlichen

Button **„Version veröffentlichen"** erzeugt einen unveränderlichen Snapshot
in `dpa_template_versions` mit SHA-256 über Inhalt. Nur veröffentlichte
Vorlagen sind im Portal sichtbar. Spätere Edits gehen in neue Versionen —
bereits abgeschlossene Verträge bleiben an ihrer ursprünglichen Version
gebunden (Reproduzierbarkeit).

## Mustache-Platzhalter

```
{{controller.company}}            ← User-Input
{{controller.representative}}      ← User-Input (oder Profil-Prefill)
{{processing.subject}}             ← User-Input (textarea)
{{processing.data_categories}}     ← Multi-Select → kommagetrennte Liste
{{tenant.legal_name}}              ← Auftragsverarbeiter (Tenant-Settings)
{{tenant.address}}                 ← Auftragsverarbeiter
{{> subprocessors}}                ← Klausel-Partial mit Key 'subprocessors'
```

Sections wie `{{#controller.dpo}}...{{/controller.dpo}}` werden nur
gerendert, wenn der Pfad truthy ist.

## Variablen-Schema

JSON-Array mit Feld-Definitionen. Beispiel:

```json
[
  {
    "key": "controller.company",
    "label": "Firma des Verantwortlichen",
    "type": "text",
    "required": true,
    "prefillFrom": "profile.company"
  },
  {
    "key": "processing.data_categories",
    "label": "Kategorien betroffener Daten",
    "type": "multi-select",
    "required": true,
    "options": [
      { "value": "contact", "label": "Kontaktdaten" },
      { "value": "health", "label": "Gesundheitsdaten (Art. 9 DSGVO)" }
    ]
  },
  {
    "key": "services.has_subprocessors",
    "label": "Unterauftragnehmer werden eingesetzt",
    "type": "toggle",
    "default": false
  }
]
```

Unterstützte `type`-Werte: `text`, `textarea`, `select`, `multi-select`,
`toggle`. `prefillFrom` derzeit nur für `profile.name` implementiert; die
anderen Felder bleiben leer und müssen vom Member ausgefüllt werden.

## Klausel-Bedingungen

```json
[
  {
    "key": "subprocessors",
    "name": "§ 5 Unterauftragsverhältnisse",
    "condition": "services.has_subprocessors",
    "content_md": "## § 5 ...\n\nDer Auftragsverarbeiter setzt ..."
  },
  {
    "key": "third_country",
    "name": "§ 10 Drittlandtransfer",
    "condition": "services.hosting_region == \"third_country\"",
    "content_md": "## § 10 ..."
  }
]
```

Unterstützte Operatoren in `condition`:

| Form | Wirkung |
|---|---|
| `path.to.value` | truthy-Check (`!empty(...)`) |
| `path == "literal"` | String-Vergleich, Bool wird als `"true"`/`"false"` interpretiert |
| `path != "literal"` | Ungleich-Vergleich |
| `path includes "literal"` | Array-Membership (für Multi-Select) |

Komplexere Logik (AND/OR) ist bewusst nicht unterstützt; bei Bedarf
zwei Klauseln mit identischem `key`-Prefix verwenden oder über
verschiedene Klausel-Keys aufteilen.

## Endkunden-Flow (Portal)

1. Member loggt sich im ePortal ein und öffnet **„AV-Verträge"**.
2. **„Neuer Vertrag"** öffnet 3-Schritt-Wizard:
   1. Vorlage + Leistungen/Datenkategorien
   2. Daten des Verantwortlichen (mit Profil-Prefill)
   3. Vorschau (PDF-Preview im neuen Tab) + Annahme-Checkbox
3. Submit erzeugt Vertrag, dispatched PDF- und Mail-Jobs.
4. Member landet auf Detail-Seite, die alle 4 s pollt bis PDF bereit ist.
5. Mail mit PDF-Attachment geht an Member-Adresse + Tenant-CC.

## Datenschutz

| Aspekt | Umsetzung |
|---|---|
| Rechtsgrundlage | Art. 6 Abs. 1 lit. b DSGVO (Vertragsanbahnung/-erfüllung) |
| Einwilligung in Speicherung | Art. 7 DSGVO: `consent_at`, `consent_text`, `consent_ip`, `consent_user_agent`, `consent_version` werden persistiert |
| Reproduzierbarkeit | `template_snapshot` (JSONB) + `content_hash` (SHA-256) garantieren byte-identische Wiedererzeugung |
| Aufbewahrung | Pro Vorlage konfigurierbar (Default 3 Jahre, BGB §195). Cron `dpa:prune` läuft täglich 03:45 UTC und hard-deleted abgelaufene Records inkl. PDF-Datei |
| Member-Account-Löschung | Verträge werden **anonymisiert** statt gelöscht: PII (E-Mail, IP, UA, Firma) werden genullt, Snapshot bleibt für Tenant-Audit (Art. 17 Abs. 3 lit. b DSGVO: Aufbewahrungspflicht überwiegt) |
| Hard-Delete via Retention | Nach Ablauf der Aufbewahrungsfrist: vollständige Löschung von Datensatz + PDF-Datei, ein Audit-Log-Eintrag ohne PII bleibt |
| Manuelles Hard-Delete | Admin kann unter `/admin/dpa-contracts` einzelne Verträge sofort hart löschen (z. B. auf Antrag des Endkunden vor Retention-Ablauf) |

## Mail-Versand

- Empfänger erhält Vertrag mit Subject „Ihr Auftragsverarbeitungsvertrag mit
  &lt;Tenant&gt;" und PDF-Attachment.
- Tenant erhält Kopie an `organizations.notification_email` (Fallback:
  `organizations.email`) mit Subject „Neuer AV-Vertrag erstellt: &lt;Firma&gt;".
- Versand erfolgt asynchron — der Job pollt sich selbst bis das PDF gerendert
  wurde und verzögert sich automatisch.
- Default-Absender = Brevo-Account; tenant-eigener DKIM/SPF-Absender ist
  Stufe 3.

## API

### Portal-Endpunkte (Sanctum + Member-Middleware)

```
GET    /api/portal/dpa/templates                — sichtbare Vorlagen
GET    /api/portal/dpa/contracts                — eigene Verträge
GET    /api/portal/dpa/contracts/{id}           — Detail
GET    /api/portal/dpa/contracts/{id}/download  — PDF-Stream
POST   /api/portal/dpa/templates/{slug}/preview — PDF-Preview (Blob)
POST   /api/portal/dpa/templates/{slug}/contracts — final, erzeugt Vertrag
```

### Admin-Endpunkte (Sanctum + Developer-Middleware + Plan-Gate)

```
GET|POST           /api/admin/dpa-templates
GET|PUT|DELETE     /api/admin/dpa-templates/{id}
POST               /api/admin/dpa-templates/{id}/fork
POST               /api/admin/dpa-templates/{id}/publish
POST               /api/admin/dpa-templates/{id}/preview

GET                /api/admin/dpa-contracts
GET                /api/admin/dpa-contracts/{id}
GET                /api/admin/dpa-contracts/{id}/download
POST               /api/admin/dpa-contracts/{id}/regenerate
DELETE             /api/admin/dpa-contracts/{id}
```

## Troubleshooting

**„PDF wird erzeugt" hängt für mehr als 1 Minute.** Queue-Worker prüfen:
`docker compose logs queue --tail=100`. Häufig: Chromium-Binary fehlt
oder `--no-sandbox` greift nicht. Konfig in `config/dpa.php`,
`BROWSERSHOT_CHROME_PATH` env muss auf Container-internen Pfad zeigen
(Default `/usr/bin/chromium`).

**PDF-Layout sieht im Render anders aus als in der Live-Vorschau im
Editor.** Live-Vorschau ist derzeit nur über PDF-Preview verfügbar (kein
HTML-Preview-Pane im Editor). Markdown-Rendering nutzt `GithubFlavored
MarkdownConverter` mit `html_input: escape` — kein Inline-HTML erlaubt.

**Klausel taucht nicht auf, obwohl Bedingung erfüllt sein sollte.**
Bedingung in `clauses[].condition` exakt notieren: keine Klammern, keine
Leerzeichen vor `==`. Test-Endpoint:
`POST /admin/dpa-templates/{id}/preview` mit `{ "input": {...} }` rendert
genau wie der Live-Wizard.

**Tenant sieht Modul-Eintrag in der Admin-Nav nicht.** Plan-Tier prüfen
(`portal` muss subscribed sein und die hoechste aktive Stufe mindestens
`business` betragen) und CHECK
`config('modules.cross_cutting_features.dpa_generator') === 'business'`.

**Member sieht Portal-Kachel „AV-Verträge" nicht.** Die Kachel ist statisch
für alle Member sichtbar; das Backend liefert aber leere
`/api/portal/dpa/templates`, wenn keine Vorlage veröffentlicht ist oder das
Plan-Feature fehlt. Tenant muss mindestens eine Vorlage forken und
publishen.

**Content-Hash ändert sich bei Regenerate.** Sollte byte-identisch sein.
Wenn nicht: Chromium-Version hat sich geändert (subpixel-rendering, Font-
Fallbacks). Lösung: Image-Version pinnen, bei gezielten Updates erneuern.

## Verwandte Code-Stellen

| Pfad | Verantwortung |
|---|---|
| `backend/app/Models/Dpa*.php` | Eloquent-Models mit Audit-/Tenant-Traits |
| `backend/app/Services/Dpa/DpaRenderService.php` | Mustache + Markdown + Browsershot |
| `backend/app/Services/Dpa/DpaContractService.php` | Orchestrierung + Anonymize + Hard-Delete |
| `backend/app/Jobs/GenerateDpaPdf.php` | Async PDF-Render |
| `backend/app/Jobs/SendDpaContractMail.php` | Async Mail mit Polling auf PDF |
| `backend/app/Mail/DpaContractMail.php` | Mailable mit dualem Audience-Subject |
| `backend/app/Console/Commands/PruneDpaContracts.php` | Retention-Cron |
| `backend/database/seeders/DpaTemplateSeeder.php` | DSK-Default-System-Template |
| `backend/resources/views/dpa/layout.blade.php` | PDF-Layout (CSS + Header/Footer) |
| `frontend/composables/useDpa.ts` | Admin-/Portal-API-Wrapper |
| `frontend/components/DpaWizardField.vue` | Schema-getriebener Feld-Renderer |
| `frontend/pages/admin/dpa-templates.vue` | Template-Editor |
| `frontend/pages/admin/dpa-contracts.vue` | Vertragsübersicht |
| `frontend/pages/portal/av-vertraege/*.vue` | Member-Liste, Wizard, Detail |

## Roadmap (Stufe 3, nicht in MVP)

- **QES via SetaPDF-Signer** für Mandanten, die qualifizierte elektronische
  Signatur nach eIDAS Art. 26 brauchen.
- **PDF-Upload-Pfad** für Mandanten mit vom Anwalt fertig gestalteter
  AV-Vorlage (FormFiller statt HTML-Render).
- **Tenant-eigener Mail-Absender** (DKIM/SPF-Setup pro Tenant).
- **Markdown-Live-Preview** im Admin-Editor.
- **Visueller Klausel-Builder** statt JSON-Editor.
