Shopware 6
Läuft in einem Shop, den wir selbst betreiben, und die ganze Kette ist von aussen nachgemessen — vom Warenkorb bis zum signierten Anfrageobjekt an die Wallet. Wenn etwas klemmt, klemmt es an Ihrem Aufbau, nicht am Paket.
Als Datei: README.md · PRUEFPLAN.md
Einbau
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
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:clearSelbst packen
Nur nötig, wenn Sie am Plugin etwas geändert haben:
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— allesedelverify_gate_banner— der Hinweis mit Knopfedelverify_gate_broken— die Meldung «nicht eingerichtet»edelverify_gate_script— Einbindung vongate.js
Eigener Auslöser an beliebiger Stelle:
<button type="button" data-edelverify>Alter bestätigen</button>Oder aus eigenem JavaScript:
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:
{% 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.xmlexistieren im Kern config.xmlvalidiert gegen die echteconfig.xsdvon 6.6- die Twig-Blöcke und -Pfade existieren an den benutzten Stellen
- der Snippet-Präfix für Warenkorbfehler (
StorefrontController::addCartErrors) - dass
customFieldsbeim Schreiben zusammengeführt statt ersetzt wird
Nicht geprüft, weil es dafür eine laufende Instanz braucht:
- ob
bin/console plugin:installdurchlä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
Prüfplan
Damit nehmen Sie den Einbau selbst ab, ohne uns zu fragen. Geht ein Punkt nicht durch, schicken Sie ihn uns mit der Nummer — dann wissen wir sofort, wo wir suchen.
Prüfplan
Dieses Plugin ist gegen den Quelltext von Shopware 6.6.10.3 abgeglichen: Jede benutzte Klasse, jede Methodensignatur, jede Dienst-Kennung, jeder Twig-Block und der Snippet-Präfix für Warenkorbfehler sind dort belegt. config.xml validiert gegen die echte config.xsd, alle PHP-Dateien gegen php -l (8.3).
Was damit nicht gezeigt ist: dass das Plugin installiert, dass der Container baut und dass eine Prüfung von Anfang bis Ende durchläuft. Genau das steht unten. Alles, was sich am Quelltext klären liess, ist aus dieser Liste entfernt — der Abschnitt «Was bereits belegt ist» am Ende sagt, was und wo, damit niemand dieselbe Arbeit zweimal macht.
Die Liste ist nach Risiko sortiert: Wer oben abbricht, braucht unten nicht weiterzulesen.
0. Umgebung
# Wegwerf-Instanz, nicht der Kundenshop
composer create-project shopware/production shopware66Gebraucht werden ausserdem:
- ein EdelVerify-Testshop mit
pk_test_…/sk_test_… - die Storefront-Domain des Testshops als erlaubte Herkunft im EdelVerify-Admin (sonst scheitert Schritt 4 und man sucht die Ursache im falschen System)
- ein Handy mit der swiyu-App und einer Beta-E-ID
1. Installation und Container
bin/console plugin:refresh
bin/console plugin:install --activate EdelVerify
bin/console cache:clearErwartet: läuft ohne Ausnahme durch. Ein Fehler beim Aufwärmen des Containers zeigt sich hier und nirgends sonst.
# Die wichtigste Zeile im ganzen Plan
bin/console debug:container --tag=shopware.cart.validator | grep -i edelverifyErwartet: AgeVerificationCartValidator steht in der Liste. Steht er nicht drin, ist das Plugin eine Attrappe — alles weitere funktioniert dann scheinbar, schützt aber nichts.
bin/console debug:router | grep edelverify
bin/console debug:event-dispatcher | grep -i edelverifyErwartet: frontend.edelverify.confirm POST /edelverify/bestaetigen, und beim Dispatcher je ein Eintrag für CheckoutOrderPlacedEvent, CheckoutCartPageLoadedEvent, CheckoutConfirmPageLoadedEvent, OffcanvasCartPageLoadedEvent.
Fehlt der Dispatcher-Eintrag für CheckoutOrderPlacedEvent, entsteht später kein Compliance-Beleg — und zwar lautlos.
2. Konfiguration
Administration → Erweiterungen → EdelVerify → Konfiguration.
- [ ] Alle drei Karten erscheinen, keine XSD-Fehlermeldung
- [ ] Kategorien- und Eigenschaften-Auswahl lädt und lässt sich befüllen
- [ ] Altersgrenze (Vorgabe) bietet genau zwei Werte an: 16 und 18, voreingestellt 18
- [ ] Die vier Ausnahmelisten («ab 16» / «ab 18», Kategorien und Eigenschaften) laden und lassen sich befüllen
- [ ] Schlüssel eintragen, speichern, Seite neu laden — Werte sind noch da
- [ ] Der geheime Schlüssel wird als Passwortfeld angezeigt
- [ ] Auf einen zweiten Verkaufskanal umschalten: Die Werte sind dort leer (Kanal-Vererbung), lassen sich getrennt setzen
bin/console system:config:get EdelVerify.config.secretKeyErwartet: der gesetzte Wert. Kommt null, stimmt der Präfix in ConfigProvider::PREFIX nicht mit dem Plugin-Namen überein.
bin/console system:config:get EdelVerify.config.restrictedCategories
bin/console system:config:get EdelVerify.config.minimumAge
bin/console system:config:get EdelVerify.config.categoriesAge16
bin/console system:config:get EdelVerify.config.categoriesAge18Erwartet: ein flaches Array aus 32-stelligen Hex-Strings. Kommt etwas anderes, greift die Fail-closed-Auslegung aus Schritt 5 — dann steht im Log Kategorie-/Eigenschaftsauswahl nicht lesbar und jeder Warenkorb gilt als prüfpflichtig. Das ist gewollt, aber der Betreiber muss es merken.
minimumAge muss 16 oder 18 sein. Steht dort etwas anderes — Import, Altbestand, ein von Hand gesetzter Konfigwert —, rechnet das Plugin mit 18 und nicht mit 16. Zum Gegenprobieren:
bin/console system:config:set EdelVerify.config.minimumAge 21Erwartet: Der Laden prüft weiterhin gegen 18, nicht gegen 21 und nicht gar nicht. Danach wieder zurückstellen.
Dasselbe für die 18er-Ausnahmeliste: Ist sie in der Datenbank unlesbar, gilt 18 für alles — auch wenn die Vorgabe auf 16 steht. Wir wüssten sonst, dass es Ware ab 18 gibt, aber nicht welche.
3. Der Riegel — ohne jede Prüfung
Der wichtigste Test, weil er ohne Handy auskommt.
- [ ] Produkt in den Warenkorb legen (Umfang = «Ganzer Shop»)
- [ ] Warenkorbseite: roter Fehler UND gelber Hinweis mit Knopf
- [ ] Kasse: dasselbe
- [ ] Bestellknopf ist deaktiviert oder die Bestellung schlägt fehl
- [ ] Direkt über die Store-API bestellen:
TOKEN=$(curl -s -X POST 'https://shop.test/store-api/context' \
-H 'sw-access-key: SWSC...' | jq -r '.token')
curl -s -X POST 'https://shop.test/store-api/checkout/cart/line-item' \
-H "sw-access-key: SWSC..." -H "sw-context-token: $TOKEN" \
-H 'content-type: application/json' \
-d '{"items":[{"type":"product","referencedId":"<produkt-uuid>","quantity":1}]}'
curl -s -X POST 'https://shop.test/store-api/checkout/order' \
-H "sw-access-key: SWSC..." -H "sw-context-token: $TOKEN"Erwartet: Die Bestellung wird nicht angelegt; die Antwort enthält CHECKOUT__CART_ERROR. Entsteht hier eine Bestellung, ist der Riegel wirkungslos — dann taugt das ganze Plugin nichts, egal wie hübsch die Storefront aussieht.
Der gelbe Hinweis ist der zweite Punkt, auf den zu achten ist. Er hängt nicht mehr am Warenkorbfehler im Template, sondern an der Seitenerweiterung page.extensions.edelverify (siehe unten, «Was bereits belegt ist», Punkt 4). Erscheint der rote Fehler, aber kein gelber Hinweis, liegt es am CheckoutPageSubscriber oder daran, dass ein Theme page_checkout_main_content ohne {{ parent() }} überschreibt.
4. Eine echte Prüfung von Anfang bis Ende
- [ ] Knopf «Alter bestätigen» öffnet das Fenster von
gate.js - [ ] QR-Code erscheint (nicht: gebrochenes Bild)
- [ ] Mit der swiyu-App scannen, bestätigen
- [ ] Fenster wird grün
- [ ] Im Netzwerk-Tab:
POST /edelverify/bestaetigenantwortet200 {"ok":true} - [ ] Seite lädt danach neu (~1,2 s), Hinweis und Fehler sind weg
- [ ] Bestellung geht durch
Die Reihenfolge der letzten drei Punkte ist der Test. Lädt die Seite neu, bevor /edelverify/bestaetigen geantwortet hat, wird die Übermittlung abgebrochen und der Nachweis ist verbrannt (er wird nur einmal ausgeliefert). Das Neuladen hängt deshalb an edelverify:stored, nicht an edelverify:verified. Im Netzwerk-Tab muss die Antwort vor dem Reload stehen.
Danach im Server-Log nachsehen:
- [ ] Weder
sk_…nochav_…taucht irgendwo auf
4b. Die zweite Schwelle — Bier ab 16, Gin ab 18
Nur nötig, wenn Sie zwei Grenzen führen. Wer alles gegen 18 prüft, überspringt diesen Abschnitt.
Vorbereitung: Vorgabe auf 18 lassen, die Bier-Kategorie in Ausnahme: Kategorien ab 16 eintragen.
| Warenkorb | Erwartet |
|---|---|
| nur Bier | Hinweis nennt 16, Prüffenster sagt «Über 16 bestätigt» |
| nur Gin | Hinweis nennt 18 |
| Bier und Gin | Hinweis nennt 18 — die strengste Grenze gewinnt |
| Bier geprüft, dann Gin dazulegen | Kasse geht wieder zu |
| danach Gin entfernen | Kasse bleibt offen, keine neue Prüfung nötig |
Die vierte Zeile ist die wichtige. Sie misst, ob ein «ja» für 16 in einen Warenkorb ab 18 hinüberträgt. Legen Sie den Gin innerhalb von `recheckSeconds` (Vorgabe 60 s) dazu — genau in diesem Fenster glaubt das Plugin seiner letzten Antwort. Bliebe die Kasse offen, wäre das Fenster nicht an die Grenze gebunden, und das Loch wäre eine Minute breit und für niemanden sichtbar.
Im Log darf dabei stehen: Nachweis reicht für diesen Warenkorb nicht. Was nicht passieren darf: dass der Nachweis dabei verworfen wird. Zeile fünf misst das — nach dem Entfernen des Gins muss der 16er-Nachweis weiter gelten.
Ohne Browser, gegen die Schnittstelle:
# 16er-Prüfung starten (Testschlüssel)
curl -s -X POST https://verify.edelbyte.ch/api/v1/verifications \
-H "x-api-key: $SK" -H 'content-type: application/json' \
-d '{"reference":"pruefplan","min_age":16,"test_outcome":"success"}'
sleep 5
# ACHTUNG: over_18 ist hier FALSE, age_ok ist TRUE
curl -s "https://verify.edelbyte.ch/api/v1/verifications/<id>" -H "x-api-key: $SK"
# Der Nachweis ab 16 löst Ware ab 18 nicht ein
curl -s -X POST https://verify.edelbyte.ch/api/v1/proofs/verify \
-H "x-api-key: $SK" -H 'content-type: application/json' \
-d '{"proof":"av_…","min_age":18}' # → valid: false`over_18` ist nicht `age_ok`. Bei einer Prüfung gegen 16 steht in
over_18auch dannfalse, wenn die Person alt genug ist. Eine Einbindung, die nur dieses Feld liest, weist jede bestandene 16er-Prüfung ab. Das Plugin liestage_ok; nachgewiesen mitpython3 scripts/pakete-pruefen.py, Abschnitt J.
5. Fail-closed, nachgestellt
Jeder Punkt muss zur Sperre führen, nie zur Freigabe.
| Test | Wie | Erwartet |
|---|---|---|
| Prüfstelle weg | baseUrl auf https://127.0.0.1:9 | Kasse zu, Antwort nach ≤ 8 s |
| Geheimer Schlüssel falsch | ein Zeichen ändern | Kasse zu, Text «nicht verfügbar» |
| Kein Schlüssel | Feld leeren | Kasse zu, Text «nicht verfügbar» |
| Nachweis verfälscht | in der Session-Datei ein Zeichen ändern | Kasse zu, Nachweis wird verworfen |
| Fremde Kennung | POST /edelverify/bestaetigen mit erfundener ID | kein ok:true |
| Prüfung noch offen | ID einer PENDING-Prüfung senden | 422 {"ok":false,"reason":"not_verified"} |
| Nachweis schon abgeholt | dieselbe ID zweimal senden | 2. Mal 409 proof_consumed |
| Schlüssel gewechselt | nach bestandener Prüfung secretKey ändern | Kasse sofort zu, nicht erst nach recheckSeconds |
| Auswahl unlesbar | restrictedCategories in der DB auf ["nonsense"] setzen, Umfang «nur ausgewählte» | jeder Warenkorb prüfpflichtig, zur strengsten Grenze, Log-Eintrag |
| Unbekannte Grenze | minimumAge auf 21 setzen | es gilt 18, nicht 21 und nicht gar nichts |
| 18er-Liste unlesbar | categoriesAge18 auf ["nonsense"], Vorgabe 16 | 18 gilt für alles |
| Nachweis zu schwach | mit 16er-Nachweis Gin bestellen | Kasse zu, Nachweis bleibt liegen |
| Plugin aus | Schalter «aktiv» aus | Bestellung geht durch, kein Netzaufruf |
Der Netzaufruf lässt sich mit tcpdump oder einem Blick ins Log der Prüfstelle belegen. Der letzte Punkt ist wichtig: Ein abgeschaltetes Plugin darf keine Latenz kosten.
Die vorletzten beiden Zeilen prüfen die zwei Stellen, an denen dieses Plugin absichtlich unbequemer ist als nötig — sie sind der Grund, warum ein Formatwechsel in Shopware oder ein Schlüsseltausch nicht in eine stille Freigabe läuft.
6. Compliance-Beleg
- [ ] Nach einer bestandenen Bestellung: Bestellung öffnen → Zusatzfelder
- [ ] Alle sechs Felder gefüllt
- [ ]
edelverify_min_ageträgt die durchgesetzte Grenze:16bei einem Bier-Warenkorb,18bei Gin — nicht pauschal 18 - [ ]
edelverify_over_18ist bei einem 16er-Nachweis nicht gesetzt - [ ]
edelverify_proof_hashist 64 Hex-Zeichen und beginnt nicht mitav_ - [ ] Felder sind schreibgeschützt
- [ ] Plugin deinstallieren mit «Daten behalten» → Werte in der DB noch da
- [ ] Plugin deinstallieren ohne → Feldsatz weg
- [ ] Zweimal installieren/deinstallieren → nur ein Feldsatz, keine Duplikate (das ist der Zweck der abgeleiteten UUID)
SELECT name FROM custom_field WHERE name LIKE 'edelverify%';Erwartet: sechs Zeilen. Kommt nichts, ist install() durchgelaufen, ohne den Feldsatz anzulegen — die Methode ist absichtlich defensiv gebaut (kein Container → still nichts tun), und genau das kann diesen Fall erzeugen. Dann gehört das Anlegen in eine MigrationStep.
7. Mehrsprachigkeit
- [ ] de-CH-Verkaufskanal: deutscher Text, keine sichtbaren Snippet-Schlüssel
- [ ] fr-CH, it-CH, en-GB ebenso
- [ ] Administration → Einstellungen → Snippets:
edelverify.banner.*findbar und änderbar
Stellen, bei denen ich mir nicht sicher bin
Was übrig bleibt, nachdem der Quelltext befragt wurde. Wer den Plan abarbeitet, sollte hier zuerst nachsehen, wenn etwas nicht tut.
Hoch
1. Wie das Custom-Field-Set entsteht
EdelVerify::install() greift über $this->container auf custom_field_set.repository zu. Das ist das Muster, das Shopware selbst vorgibt (Core/Framework/Plugin/Command/Scaffolding/stubs/plugin-class-with-custom-fields.stub tut genau dasselbe), also stimmt der Weg. Offen bleibt der Zeitpunkt: In welcher Bootphase install() läuft und ob der Container dort schon steht, zeigt nur ein Lauf. Die Methode ist defensiv gebaut (kein Container → still nichts tun) — das schliesst einen Fatal Error aus und ermöglicht dafür den umgekehrten Fehler: Die Installation läuft durch, die Felder fehlen.
Prüfen: die SQL-Abfrage aus Schritt 6.
2. Themes, die `page_checkout_main_content` kapern
Der Hinweis hängt jetzt an den konkreten Seiten (page/checkout/cart/index.html.twig, .../confirm/index.html.twig) und ruft {{ parent() }} auf. Das ist der belegte Weg. Ein Theme, das denselben Block ohne {{ parent() }} überschreibt, hebelt ihn trotzdem aus — je nach Reihenfolge in der Template-Hierarchie.
Prüfen: mit dem Kundentheme, nicht nur mit dem Standardtheme. Fehlt der gelbe Hinweis, während der rote Fehler steht, ist es das. Der Riegel ist davon nicht betroffen; es geht nur um die Bedienbarkeit.
Mittel
3. Session im Store-API-Kontext
ProofStore holt die Sitzung über $request->hasSession(). In der Storefront ist das eine gewöhnliche Symfony-Session. Ob der Store-API-Kontext eine hat, ist ungeprüft — falls ja, könnte ein Nachweis dort landen, wo er nicht hingehört. Für den Riegel ist das ungefährlich (er prüft trotzdem gegen die Prüfstelle), für die Sauberkeit nicht schön. Die Route selbst ist über _routeScope: storefront gegen Store-API-Zugriff abgeschirmt.
Prüfen: in confirm() mitloggen, ob store() true liefert; und einmal über die Store-API dieselbe Route ansprechen (erwartet: 404).
4. Snippet-Sets für de-CH / fr-CH / it-CH
Shopware liefert von Haus aus de-DE und en-GB. Ob ein de-CH-Verkaufskanal messages.de-CH.json heranzieht oder auf de-DE zurückfällt, hängt am angelegten Snippet-Set. Deshalb liegt de-DE als Kopie bei. fr-CH und it-CH haben keine Rückfallebene — läuft ein Kanal auf fr-FR statt fr-CH, stehen dort englische oder rohe Texte.
Behebung, falls nötig: die Dateien nach fr_FR/messages.fr-FR.json und it_IT/messages.it-IT.json kopieren.
5. Inline-`<script>` und CSP
gate.html.twig enthält ein kleines Inline-Skript für das Neuladen. Läuft der Shop mit strikter Content-Security-Policy ohne unsafe-inline, wird es blockiert: Die Prüfung gelingt, die Seite lädt aber nicht nach, und der Kunde sieht weiterhin den Hinweis.
Behebung: in eine Datei unter Resources/app/storefront/dist/ auslagern — dann ist allerdings ein Storefront-Build nötig.
Niedrig
6. HTTP-Client ohne injizierten Dienst
EdelVerifyClient baut sich mit HttpClient::create() selbst einen Client. Steht der Shop hinter einem Proxy oder braucht ein eigenes CA-Bündel, greifen dessen Einstellungen nicht. Dann in services.xml den Dienst http_client als zweites Argument nachreichen (die Stelle ist dort auskommentiert vorbereitet).
7. `app.request.locale` für `data-lang`
Liefert je nach Konfiguration de, de-DE oder de-CH. gate.js schneidet selbst auf zwei Zeichen, also unkritisch. Kommt eine Sprache heraus, die gate.js nicht kennt, fällt es auf Deutsch zurück.
8. Kein Verbindungstest im Backend
Die WooCommerce-Fassung hat einen Knopf, der Erreichbarkeit, sk_, pk_ und Herkunftsprüfung in einem Durchgang beantwortet. Hier fehlt er. Ersatz von Hand:
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://verify.edelbyte.ch/api/v1/proofs/verify \
-H 'content-type: application/json' -H "x-api-key: sk_live_…" \
-d '{"proof":"verbindungstest"}'
# 200 = Schlüssel gut | 401 = Schlüssel abgelehnt
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://verify.edelbyte.ch/api/v1/verifications \
-H 'content-type: application/json' -H "authorization: Bearer pk_live_…" \
-H 'origin: https://shop.test' -d '{"reference":"verbindungstest"}'
# 201 = Schlüssel und Herkunft gut | 401 = eines von beidem falschEin solcher Knopf wäre die nächste sinnvolle Ergänzung — «es geht nicht» hat bei einer Altersprüfung fast immer eine dieser drei Ursachen.
Was bereits belegt ist
Diese Punkte standen früher in der Zweifelsliste. Sie sind am Quelltext von 6.6.10.3 geklärt und gehören nicht mehr geprüft. Die Befehle stehen dabei, falls jemand nachrechnen will ($SW = Wurzel des Shopware-Quellbaums).
| Frage | Antwort | Beleg |
|---|---|---|
| Snippet-Präfix für Warenkorbfehler | checkout.<messageKey> | StorefrontController::addCartErrors() baut 'checkout.' . $error->getMessageKey(). Das mitgelieferte cart-alerts.html.twig benutzt für Fehler dagegen error.<messageKey> — deshalb liegen beide Präfixe in den Snippet-Dateien. |
Ist ErrorCollection nach getId() indiziert? | ja | ErrorCollection::add() ruft $this->set($error->getId(), $error); Collection::has() ist public. |
Gibt es page_checkout_main_content? | ja, aber als Einhängepunkt untauglich | Der Block in _page.html.twig ist leer, und cart/index.html.twig wie confirm/index.html.twig überschreiben ihn ohne {{ parent() }}. Eine Überschreibung im Elterntemplate rendert nichts. Deshalb erweitert das Plugin die konkreten Seiten. |
page.cart.errors im Template? | unbrauchbar | CheckoutController::cartPage() ruft $cartErrors->clear() unmittelbar vor renderStorefront(). Auf der Warenkorbseite ist die Sammlung beim Rendern leer. Der Stand kommt jetzt über page.extensions.edelverify, gesetzt im Seitenereignis — das feuert im PageLoader, also vor dem Aufräumen. |
Annotation\Route oder Attribute\Route? | Attribute\Route | Shopware 6.6 verlangt symfony/* ~7.2; in Symfony 7 gibt es Annotation\Route nicht mehr. Alle 28 Storefront-Controller des Kerns benutzen Attribute\Route. routes.xml mit type="attribute" ist ebenfalls die Kernschreibweise. |
Namensraum von CheckoutOrderPlacedEvent | Shopware\Core\Checkout\Cart\Event | find $SW/src -name CheckoutOrderPlacedEvent.php. Nicht Checkout\Order\Event. getSalesChannelId() existiert. |
Überschreibt ein Order-Update fremde customFields? | nein | CustomFieldsSerializer::encode() erzeugt für vorhandene Zeilen ein JsonUpdateCommand; EntityWriteGateway setzt das als JSON_SET(IFNULL(custom_fields, "{}"), …) um. |
Wirft verschachteltes Context::scope() eine Ausnahme? | nein | Context::scope() stellt den vorherigen Scope in einem finally wieder her. |
Speichert sw-entity-multi-id-select UUID-Strings oder Objekte? | flache UUID-Strings | Die Komponente gibt collection.getIds() weiter; sw-form-field-renderer baut aus <entity> das nötige repository. Trotzdem ist ConfigProvider::idList() so gebaut, dass ein unerwartetes Format zur Sperre führt statt zu einer leeren Liste — das war ein Fail-open. |
Gibt es den Dienst logger? | ja | Vom Kern selbst benutzt, u. a. in Core/Framework/DependencyInjection/app.xml. |
Gibt es shopware.http_client? | nein | Der Dienst heisst http_client. Der Hinweis in services.xml war falsch und ist korrigiert. |
Blockiert blockOrder() wirklich die Bestellung? | ja | OrderPersister::persist() wirft bei $cart->getErrors()->blockOrder() eine CartException::invalidCart() — vor jedem Schreibvorgang, also auch für die Store-API. |
Validiert config.xml gegen die XSD? | ja | php -r '…schemaValidate…' src/Resources/config/config.xml $SW/src/Core/System/SystemConfig/Schema/config.xsd → VALID. |
Reicht Shopware getParameters() an die Übersetzung durch? | ja | StorefrontController::addCartErrors() ruft $this->trans('checkout.' . $error->getMessageKey(), $error->getParameters()). Deshalb steht %minAge% in den Snippets und die Grenze im Fehlertext. |
Verhält sich min_age an der Schnittstelle wie beschrieben? | ja, nachgemessen | python3 scripts/api-vertragstest.py, Abschnitt 10 — gegen verify.edelbyte.ch: 16/18 werden angenommen, 17/21/"18"/null/0 mit 400 invalid_min_age abgewiesen, over_18 ist bei einer bestandenen 16er-Prüfung false, age_ok true. |
Liest das Plugin irgendwo over_18 als alleinige Freigabe? | nein | python3 scripts/pakete-pruefen.py, Abschnitt J. Die Prüfung hat eine Selbstprüfung: Schnipsel, die auffallen müssen, und solche, die nicht auffallen dürfen. |
| Unterscheidet der Zwischenspeicher die Grenzen? | ja | python3 scripts/pakete-pruefen.py, Abschnitt K. Sowohl AgeGate::$memo als auch ProofStore::lastCheckedAt() sind an die verlangte Grenze gebunden. |