# EdelVerify für Shopware 6

Altersprüfung mit der staatlichen Schweizer E-ID (swiyu). Das Plugin sperrt den
Checkout für altersbeschränkte Artikel, bis bestätigt ist, dass die kaufende
Person die verlangte Altersgrenze erfüllt. Der Shop erfährt genau diese eine
Angabe — keinen Namen, kein Geburtsdatum, keine Ausweiskopie.

Die Grenze ist einstellbar: **16 oder 18**, je Verkaufskanal als Vorgabe und je
Kategorie oder Eigenschaft als Ausnahme. Bei gemischtem Warenkorb gilt die
strengste. Ein Kanal, der nur die Vorgabe setzt, verhält sich exakt wie vor der
zweiten Schwelle.

Erfordert Shopware 6.6 und PHP 8.2.

---

## Wie der Riegel funktioniert

Es gibt zwei Ebenen, und nur eine davon ist verbindlich.

**Im Browser** bindet das Plugin `gate.js` von verify.edelbyte.ch ein. Das
Skript zeigt ein Fenster mit QR-Code und Deeplink in die swiyu-App. Es ist
Bedienung, kein Schutz — wer es mit den Entwicklerwerkzeugen wegräumt, hat
nichts gewonnen.

**Auf dem Server** hängt ein `CartValidator` im Warenkorb. Solange kein
gültiger Nachweis in der Sitzung liegt, setzt er einen blockierenden Fehler.
Der `OrderPersister` von Shopware weigert sich dann, eine Bestellung zu
schreiben. Das gilt für den Storefront-Checkout genauso wie für einen direkten
Zugriff auf die Store-API, denn beide laufen durch dieselbe Stelle.

Der Ablauf im Ganzen:

```
Browser                Shop-Server                    verify.edelbyte.ch
   │                        │                                 │
   │ 1. Prüffenster öffnen  │                                 │
   ├────────────────────────┼── POST /verifications (pk_) ───►│
   │◄───────────────────────┼─── QR + Deeplink ───────────────┤
   │                        │                                 │
   │ 2. swiyu-App, Kunde bestätigt                            │
   │                        │                                 │
   │ 3. POST /edelverify/bestaetigen                          │
   │    { verification_id } │                                 │
   ├───────────────────────►│                                 │
   │                        ├── GET /verifications/:id (sk_) ►│
   │                        │◄── SUCCESS + Nachweis ──────────┤
   │                        ├── POST /proofs/verify (sk_) ───►│
   │                        │◄── valid: true ─────────────────┤
   │                        │  Nachweis → Sitzung             │
   │◄───────────────────────┤                                 │
   │                        │                                 │
   │ 4. Seite neu laden → CartValidator findet den Nachweis   │
   │                        ├── POST /proofs/verify (sk_) ───►│
   │                        │  → Kasse frei                   │
```

Der geheime Schlüssel `sk_` verlässt den Shop-Server nie. Der Browser sieht nur
den öffentlichen Schlüssel `pk_` und die Kennung der Prüfung — beides für sich
wertlos.

---

## Fail-closed

Jeder Zweifel führt zur Sperre, nie zur Freigabe. Das gilt für:

| Situation | Verhalten |
|---|---|
| Kein Nachweis in der Sitzung | Kasse gesperrt |
| Prüfstelle nicht erreichbar / Zeitüberschreitung | Kasse gesperrt |
| Antwort 500 oder kaputtes JSON | Kasse gesperrt |
| Geheimer Schlüssel fehlt oder wird abgelehnt | Kasse gesperrt (eigener Text) |
| Produkteinstufung nicht lesbar | Warenkorb gilt als prüfpflichtig, zur strengsten Grenze |
| Unbekannte Altersgrenze in der Datenbank | gilt als 18, nicht als 16 |
| Ausnahmeliste «ab 18» nicht lesbar | 18 gilt für alles |
| Nachweis ab 16, Ware ab 18 | Kasse gesperrt — auch innerhalb des Prüffensters |
| Antwort passt nicht zur Anfrage (`required_min_age`) | Kasse gesperrt, Fehler im Log |

Der einzige vorgesehene Weg, den Riegel zu öffnen, ist der Schalter
**Altersprüfung aktiv** in der Administration. Ein Konfigurationsfehler darf
eine Altersprüfung nicht abschalten können.

---

## Was Sie vorher brauchen

**Das Paket:** https://verify.edelbyte.ch/v1/shopware/edelverify-shopware.zip —
darin liegt der Ordner `EdelVerify` samt dieser Anleitung und dem Prüfplan.

**Ein Schlüsselpaar von uns.** Einen Selbstbedienungszugang gibt es heute
nicht: Es gibt kein Anmeldeformular, in dem Sie sich ein Konto anlegen und
Schlüssel abholen. Wir legen Ihren Shop von Hand an und schicken Ihnen den
öffentlichen (`pk_test_…`) und den geheimen (`sk_test_…`). Im selben Zug
hinterlegen wir die erlaubten Herkunftsadressen — auch das ist nichts, was Sie
selbst einstellen können.

Der Weg dorthin ist ein kurzes Gespräch:

* Warteliste: https://verify.edelbyte.ch/#warteliste
* E-Mail: info@edelbyte.ch
* Telefon: 044 500 25 04

Sagen Sie uns dabei die Domains aller Verkaufskanäle (mit und ohne `www`, dazu
das Testsystem) und die Altersgrenze, die Ihre Ware verlangt.

**Was es kostet: heute nichts** — weil heute nichts verkauft wird. Es gibt
keinen Preis, kein Abonnement und keine Rechnung. Wir nennen bewusst auch
keinen Betrag «ab», solange die produktive E-ID nicht steht. Was es gibt, ist
ein Pilotgespräch, Testschlüssel zum Bauen und ein Platz auf der Warteliste.
Über Konditionen sprechen wir, bevor Sie scharf schalten — nicht danach.

Die vollständige technische Anleitung — Schnittstelle, Fehlerschlüssel,
Webhook, Testausgänge — steht unter https://verify.edelbyte.ch/entwickler

---

## Installation

### Als ZIP über die Administration

Das fertige Archiv liegt hier:
https://verify.edelbyte.ch/v1/shopware/edelverify-shopware.zip

In der Administration unter **Erweiterungen → Meine Erweiterungen →
Erweiterung hochladen** einspielen und aktivieren. Der oberste Ordner im
Archiv heisst `EdelVerify` und muss genau so heissen — sonst erkennt Shopware
das Plugin nicht.

### Aus einem Verzeichnis

```bash
cp -r EdelVerify /pfad/zu/shopware/custom/plugins/EdelVerify

cd /pfad/zu/shopware
bin/console plugin:refresh
bin/console plugin:install --activate EdelVerify
bin/console cache:clear
```

### Selbst packen

Nur nötig, wenn Sie am Plugin etwas geändert haben:

```bash
cd /pfad/zu/plugins
zip -r EdelVerify.zip EdelVerify -x '*/.git/*'
```

Ein Storefront-Build ist **nicht** nötig: Das Plugin bringt kein eigenes
JavaScript-Bundle mit, sondern lädt `gate.js` direkt von verify.edelbyte.ch.
`bin/build-storefront.sh` kann entfallen; ein Cache-Clear genügt.

---

## Konfiguration

**Erweiterungen → Meine Erweiterungen → EdelVerify → ⋯ → Konfiguration**

Oben rechts steht die Auswahl des Verkaufskanals. Jede Einstellung lässt sich je
Kanal überschreiben — nützlich, wenn neben dem Schweizer Endkundenshop ein
B2B-Kanal ohne Prüfung läuft.

### Verbindung zu EdelVerify

| Feld | Bedeutung |
|---|---|
| **Altersprüfung aktiv** | Aus = kein Riegel, kein Netzaufruf. Der einzige vorgesehene Ausschalter. |
| **Adresse des Dienstes** | `https://verify.edelbyte.ch`, nur bei eigener Instanz ändern. |
| **Öffentlicher Schlüssel** | `pk_live_…` — steht im Quelltext der Ladenseite, so vorgesehen. |
| **Geheimer Schlüssel** | `sk_live_…` — verlässt den Server nie. |

Beide Schlüssel bekommen Sie von uns (siehe «Was Sie vorher brauchen»).
Fangen Sie mit den Testschlüsseln an — `pk_test_…` und `sk_test_…`: Damit
läuft die Prüfung vollständig durch, ohne dass jemand eine Wallet zückt.

> **Wichtig:** Der öffentliche Schlüssel unterliegt einer Herkunftsprüfung. Die
> Domain jedes Verkaufskanals, in dem geprüft wird, muss bei uns als erlaubte
> Herkunft hinterlegt sein — sonst öffnet sich im Laden zwar das Fenster, aber
> es lässt sich keine Prüfung starten. Das ist der häufigste
> Einrichtungsfehler. Es gibt dafür **kein Feld im Plugin**: Schicken Sie uns
> die Domains an info@edelbyte.ch, wir tragen sie ein.

### Was geprüft wird

**Umfang: Ganzer Shop** ist die Vorgabe. Jeder gefüllte Warenkorb verlangt eine
Prüfung. Richtig für reine Spirituosen-, Tabak- oder Vape-Läden.

**Umfang: Nur ausgewählte Kategorien und Eigenschaften** für gemischte
Sortimente. Zwei Wege, die ODER-verknüpft sind:

- **Betroffene Kategorien** — Unterkategorien zählen mit, weil Shopware den
  ganzen Kategoriebaum eines Produkts mitführt. Es genügt also, die oberste
  Kategorie zu wählen.
- **Betroffene Eigenschaften** — für Sortimente, die sich nicht sauber nach
  Kategorie trennen lassen. Typisch: eine Eigenschaft «Altersbeschränkt: ja».

> Ist «Nur ausgewählte …» gewählt, aber **nichts** ausgewählt, wird **nicht**
> geprüft. Das ist Absicht — «alles prüfen» wäre bei einer halbfertigen
> Einrichtung eine Überraschung, und die Wahl «Ganzer Shop» steht direkt
> daneben.

**Altersgrenze (Vorgabe)** kennt nur 16 und 18 — mehr belegt die staatliche
E-ID nicht. Ein Feld, das man auf 21 stellen kann, ohne dass es wirkt, wäre eine
Lüge im Backend. Die Vorgabe gilt für alles, was in den beiden Ausnahmelisten
nicht genannt ist. Ab Werk 18.

**Ausnahme: Kategorien / Eigenschaften ab 16** — Ware, die nur eine Prüfung ab
16 verlangt. Bier, Wein, Cider.

**Ausnahme: Kategorien / Eigenschaften ab 18** — Ware, die immer 18 verlangt.
Nötig, wenn die Vorgabe auf 16 steht. Diese Liste sticht die 16er-Liste: Was in
beiden steht, wird ab 18 geprüft. Eine Lockerung durch Doppelzuordnung fiele
sonst niemandem auf.

> **Bei gemischtem Warenkorb gilt die strengste Grenze.** Ein Sixpack neben
> einer Flasche Gin macht den ganzen Warenkorb zu einem 18er-Warenkorb. Legt
> jemand nach bestandener 16er-Prüfung Spirituosen dazu, geht die Kasse wieder
> zu und verlangt eine neue Prüfung — auch innerhalb des Prüffensters unten.
> Umgekehrt nicht: Ein Nachweis ab 18 löst auch Ware ab 16 ein.

### Feineinstellung

**Nachweis erneut prüfen nach (Sekunden)**, Vorgabe 60. Der Warenkorb wird bei
jedem Seitenaufruf neu berechnet; ohne dieses Fenster liefe jedes Mal ein
Netzaufruf mit. `0` heisst: bei jeder Berechnung neu fragen — maximal streng,
spürbar langsamer. Werte über 300 werden auf 300 gekürzt.

Das Fenster schwächt den Riegel praktisch nicht: Nachweise laufen in Tagen ab,
nicht in Sekunden. Es verkürzt nur die Reaktionszeit auf einen zurückgezogenen
Nachweis auf höchstens diese Spanne.

---

## Der Compliance-Beleg

Nach jeder Bestellung, für die ein Nachweis vorlag, vermerkt das Plugin an der
Bestellung:

| Custom Field | Inhalt |
|---|---|
| `edelverify_verification_id` | Kennung der Prüfung |
| `edelverify_proof_hash` | SHA-256-Abdruck des Nachweises |
| `edelverify_min_age` | die **durchgesetzte** Grenze dieser Bestellung — 16 oder 18 |
| `edelverify_over_18` | ob der vorgelegte Nachweis auf «über 18» lautet |
| `edelverify_checked_at` | Zeitpunkt der letzten Prüfung (ISO 8601, UTC) |
| `edelverify_proof_expires_at` | Gültigkeit des Nachweises |

Zu sehen in der Administration unter **Bestellungen → Bestellung öffnen →
Zusatzfelder**.

**Warum überhaupt:** Der Riegel entscheidet richtig, hinterlässt aber nichts.
Der Nachweis liegt in der Sitzung und ist nach Stunden weg. Kommt später die
Frage — kantonale Aufsicht, Testkauf, Rechtsstreit —, ob eine bestimmte
Bestellung geprüft war, ist «unser Plugin lässt das nicht zu» kein Beleg. Ein
Datensatz an der Bestellung ist einer, und Bestellungen werden aufbewahrt,
gesichert und exportiert. Logfiles werden rotiert.

**Warum nur ein Abdruck und nicht der Nachweis:** Die Zeichenfolge `av_…` ist
bis zu 30 Tage gültig und wirkt wie ein Passwort — wer sie hat, kann in diesem
Shop bestellen. In der Bestelltabelle läge sie im Klartext und wäre über jedes
Backup und jeden Admin-Zugang lesbar. Der Abdruck reicht für den einen Zweck,
den er hat (belegen, dass zwei Bestellungen denselben Nachweis benutzt haben
oder eben nicht) und ist wertlos für jeden, der damit bestellen will.

Beim Deinstallieren mit **«Daten behalten»** bleiben die Felder erhalten. Das
ist der Regelfall: Wer das Plugin entfernt, soll nicht die Belege alter
Bestellungen verlieren.

---

## Anpassen im Theme

Der Hinweis wird über
`@EdelVerify/storefront/component/edelverify/gate.html.twig` gerendert und
benutzt ausschliesslich Bootstrap-Klassen der Storefront (`alert`,
`btn btn-primary`) — er passt sich also dem Theme an, ohne eigenes CSS
mitzubringen.

Überschreibbare Twig-Blöcke:

- `edelverify_gate` — alles
- `edelverify_gate_banner` — der Hinweis mit Knopf
- `edelverify_gate_broken` — die Meldung «nicht eingerichtet»
- `edelverify_gate_script` — Einbindung von `gate.js`

Eigener Auslöser an beliebiger Stelle:

```twig
<button type="button" data-edelverify>Alter bestätigen</button>
```

Oder aus eigenem JavaScript:

```js
EdelVerify.open({ reference: 'warenkorb-42' })
  .then((r) => { if (r.verified) location.reload() })
```

Ereignisse am `document`: `edelverify:started`, `edelverify:verified`,
`edelverify:stored`, `edelverify:failed`, `edelverify:error`,
`edelverify:closed`.

**Wer die Seite neu lädt, tut das auf `edelverify:stored` — nie auf
`edelverify:verified`.** `verified` heisst nur, dass die Prüfung bestanden ist;
die Meldung an den Shop läuft in diesem Augenblick erst los. Ein Neuladen
bricht sie ab, und der Nachweis wird nur ein einziges Mal ausgeliefert — er ist
dann verloren und die Kasse bleibt zu.

Ob der Riegel gerade greift, steht als Erweiterung an der Seite und lässt sich
in jedem Checkout-Template abfragen:

```twig
{% if page.extensions.edelverify is defined and page.extensions.edelverify.needed %}…{% endif %}
```

Gesetzt wird sie auf der Warenkorbseite, in der Kasse und im
Offcanvas-Warenkorb. Nicht über `page.cart.errors` gehen: Der Controller leert
die Fehlersammlung, bevor Twig läuft.

Texte werden über die Snippets `edelverify.banner.*` und
`checkout.edelverifyAgeRequired` / `checkout.edelverifyNotConfigured` geändert —
in der Administration unter **Einstellungen → Snippets**, ohne Code. Dieselben
zwei Texte liegen zusätzlich unter `error.*`, weil das mitgelieferte
`cart-alerts.html.twig` diesen Präfix benutzt; wer sie ändert, ändert beide.

---

## Was dieses Plugin NICHT tut

Ehrlichkeitshalber, damit niemand mehr erwartet als da ist:

- **Kein Hinweis im Offcanvas-Warenkorb.** Der Riegel greift dort selbst-
  verständlich (es ist derselbe Warenkorb), und Shopware zeigt den Fehlertext
  an. Aber der Knopf «Alter bestätigen» erscheint nur auf der Warenkorbseite
  und in der Kasse.
- **Keine Prüfung auf der Produktseite.** Wer ein altersbeschränktes Produkt
  ansieht, wird nicht gefragt. Geprüft wird beim Bestellen.
- **Keine Administrations-Oberfläche zum Nachschauen.** Es gibt keine Liste
  «alle geprüften Bestellungen». Die Belege stehen je Bestellung in den
  Zusatzfeldern; für Auswertungen ist der Weg über die Admin-API oder ein
  Bestell-Export.
- **Kein Verbindungstest-Knopf.** Die WooCommerce-Fassung hat einen; hier fehlt
  er. Die Prüfung von Hand steht im PRUEFPLAN.md.
- **Kein eigenes Rate-Limit** auf `/edelverify/bestaetigen`. Die Route kostet
  bei Missbrauch Netzaufrufe zu EdelVerify, aber sie kann keinen Nachweis
  erzeugen. Wer sie absichern will, nimmt die Rate-Limiter-Konfiguration von
  Shopware oder den Reverse Proxy.
- **Keine automatisierten Tests.** Siehe unten.

---

## Ungetestet

**Dieses Plugin wurde noch nie auf einer Shopware-Instanz ausgeführt.** Es ist
gegen die Schnittstelle von EdelVerify geschrieben (die ist verifiziert) und
gegen den Quelltext von Shopware 6.6.10.3 abgeglichen — jede benutzte Klasse,
Methode, Dienst-Kennung und jeder Twig-Block ist dort belegt. Was daraus
folgt: Es sollte laden. Was daraus **nicht** folgt: dass es tut, was es soll.

Statisch abgeglichen (nicht mehr offen):

- alle Klassen und Interfaces existieren, alle Signaturen passen
- alle Dienst-Kennungen in `services.xml` existieren im Kern
- `config.xml` validiert gegen die echte `config.xsd` von 6.6
- die Twig-Blöcke und -Pfade existieren an den benutzten Stellen
- der Snippet-Präfix für Warenkorbfehler (`StorefrontController::addCartErrors`)
- dass `customFields` beim Schreiben zusammengeführt statt ersetzt wird

Nicht geprüft, weil es dafür eine laufende Instanz braucht:

- ob `bin/console plugin:install` durchläuft
- ob der Container beim Aufwärmen tatsächlich baut
- ob das Custom-Field-Set bei der Installation korrekt entsteht
- ob eine echte Prüfung von Anfang bis Ende durchläuft

Die konkreten Schritte, mit denen sich das nachholen lässt, und die Stellen, an
denen Zweifel bestehen, stehen in **[PRUEFPLAN.md](PRUEFPLAN.md)**. Vor dem
ersten produktiven Einsatz ist der abzuarbeiten.

---

## Lizenz

MIT. © EdelByte — https://edelbyte.ch
