# EdelVerify für Magento 2 — Altersprüfung mit der Schweizer E-ID

Sperrt den Bestellabschluss für altersbeschränkte Artikel, bis über die
staatliche Schweizer E-ID (swiyu) bestätigt ist, dass die Kundin oder der Kunde
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 Store-View als Vorgabe und je
Kategorie als Ausnahme. Bei gemischtem Warenkorb gilt die strengste. Ein
Store-View, der nur die Vorgabe setzt, verhält sich exakt wie vor der zweiten
Schwelle.

Gegen den Quelltext von **Magento Open Source 2.4.7-p3** abgeglichen — jede
Klasse, jede Signatur und jeder Ereignisname ist dort nachgeschlagen. **Gelaufen
ist das Modul noch in keiner Magento-Instanz.** Was das offenlässt, steht
vollständig in `PRUEFPLAN.md`; arbeiten Sie diesen Plan ab, bevor Sie das Modul
produktiv einsetzen.

---

## Wie es funktioniert

```
Browser                    Magento-Server                 verify.edelbyte.ch
───────                    ──────────────                 ──────────────────
Knopf «Alter bestätigen»
  → gate.js öffnet das Fenster ──────────────────────────→ POST /verifications
  ← QR-Code, Deeplink   ←──────────────────────────────── (mit pk_…)
  swiyu-App, Wallet bestätigt
  → verification_id  ──→ POST /edelverify/confirm/index
                           holt den Nachweis ───────────→ GET /verifications/:id
                                                          (mit sk_…)
                         legt `av_…` in die Sitzung
Bestellung abschicken  ──→ CartManagementInterface::placeOrder()
                           löst den Nachweis ein ───────→ POST /proofs/verify
                           mit min_age = was die WARE verlangt
                           valid ? weiter : CouldNotSaveException
```

Drei Punkte, auf die es ankommt:

* **Der Browser sieht den geheimen Schlüssel nie.** Er meldet nur die Kennung
  der Prüfung. Den Nachweis holt der Shop-Server selbst.
* **Der Riegel sitzt serverseitig.** Wer den Knopf mit den Entwicklerwerkzeugen
  entfernt, kommt trotzdem nicht durch.
* **Fail-closed überall.** Kein Nachweis, kein Netz, kein Schlüssel, unklare
  Antwort → Bestellung abgewiesen. Nie freigegeben. Bei einer Altersprüfung ist
  die sichere Richtung die unbequeme.

---

## Was Sie vorher brauchen

**Das Paket:** https://verify.edelbyte.ch/v1/magento/edelverify-magento.zip —
darin liegt der Baum `EdelByte/EdelVerify` samt dieser Anleitung und dem
Prüfplan. Genau so gehört er nach `app/code/`.

**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 Basis-URLs aller Store-Views (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

### Variante A — Dateien kopieren

```bash
cp -r EdelByte <magento-root>/app/code/
cd <magento-root>
bin/magento module:enable EdelByte_EdelVerify
bin/magento setup:upgrade
bin/magento setup:di:compile          # nur im Produktionsmodus nötig
bin/magento setup:static-content:deploy de_CH fr_CH it_CH en_US
bin/magento cache:flush
```

### Variante B — über Composer

```bash
composer config repositories.edelverify path ./EdelByte/EdelVerify
composer require edelbyte/module-edelverify:^1.0
bin/magento setup:upgrade
```

`setup:upgrade` legt fünf Spalten an `sales_order` an (siehe *Beleg an der
Bestellung*). Ohne diesen Schritt läuft der Riegel, aber der Beleg wird still
verworfen.

---

## Konfiguration

**Stores → Konfiguration → Verkäufe → EdelVerify — Altersprüfung**

| Feld | Bedeutung |
|---|---|
| Altersprüfung aktiv | Der Hauptschalter. Vorgabe: **Nein**. |
| Adresse des Dienstes | `https://verify.edelbyte.ch`. Nur ändern, wenn Sie EdelVerify selbst betreiben (siehe *Content-Security-Policy*). |
| Öffentlicher Schlüssel | `pk_live_…` / `pk_test_…`. Steht im Quelltext der Seite — das ist so vorgesehen. |
| Geheimer Schlüssel | `sk_live_…` / `sk_test_…`. Wird verschlüsselt abgelegt (`type="obscure"` + `Magento\Config\Model\Config\Backend\Encrypted`). Leer lassen behält den bestehenden Wert. |
| Altersgrenze (Vorgabe) | `ab 16` oder `ab 18`. Vorgabe: **ab 18**. Gilt, wo keine Kategorie etwas anderes sagt. |
| Betroffene Kategorien | Kategorie-**IDs**, mit Komma getrennt, mit optionaler eigener Grenze. **Leer heisst: der ganze Shop.** |

Alle Felder gelten je Store-View. Ein zweisprachiger Shop mit zwei Sortimenten
kann die Prüfung also in einem Store-View führen und im anderen nicht.

Beide Schlüssel bekommen Sie von uns (siehe *Was Sie vorher brauchen*); fangen
Sie mit den Testschlüsseln an. Für die **erlaubte Herkunft** gibt es hier kein
Feld: Die Liste liegt bei uns. Schicken Sie uns die Basis-URLs Ihrer
Store-Views an info@edelbyte.ch — solange für Ihren Shop keine darin steht,
darf jede beliebige Seite Prüfungen auf Ihren öffentlichen Schlüssel starten.

### Zu den Kategorien

Es werden **IDs** eingetragen, keine URL-Schlüssel. Die ID einer Kategorie steht
im Kategoriebaum hinter dem Namen. Grund: Die ID überlebt eine Umbenennung und
einen Sprachwechsel, der URL-Schlüssel nicht — und eine Altersprüfung, die nach
dem Umbenennen einer Kategorie stillschweigend aufhört zu greifen, ist
schlimmer als keine.

**Unterkategorien werden nicht mitgeprüft.** Wer «Tabak» einträgt und die Ware
in der Unterkategorie «Zigarren» führt, prüft nichts. Tragen Sie die
Unterkategorien einzeln ein, oder lassen Sie das Feld leer.

### Zur Altersgrenze

Die Vorgabe gilt überall dort, wo keine Kategorie etwas anderes sagt. Eine
eigene Grenze hängen Sie mit Doppelpunkt an die ID:

    17:16,18:16,12:18,15

Kategorie 17 und 18 verlangen 16, Kategorie 12 verlangt 18, Kategorie 15 die
Vorgabe. Ein Eintrag ohne Zusatz verhält sich wie vor der zweiten Schwelle —
jede bestehende Einstellung bleibt also Zeichen für Zeichen gültig.

Ein Zusatz, den der Dienst nicht kennt (`:17`), wird auf **18** angehoben, nicht
auf 16 gesenkt. Dasselbe gilt für einen unbekannten Wert der Vorgabe: Im Zweifel
prüft das Modul strenger, nicht milder.

**Bei gemischtem Warenkorb gilt die strengste Grenze.** Ein Sixpack neben einer
Flasche Gin macht den ganzen Warenkorb zu einem 18er-Warenkorb. Liegt ein
Artikel in zwei eingetragenen Kategorien mit verschiedenen Grenzen, gilt
ebenfalls die strengere — eine Lockerung durch Doppelzuordnung fiele niemandem
auf.

Ein Nachweis ab 18 löst auch Ware ab 16 ein; umgekehrt nicht. Wer sich für Bier
ausgewiesen hat und dann Spirituosen dazulegt, wird erneut gefragt.

### Content-Security-Policy

`etc/csp_whitelist.xml` gibt `verify.edelbyte.ch` für `script-src`,
`connect-src` und `img-src` frei. Wer eine **andere** Adresse einträgt, muss sie
dort ergänzen — eine CSP lässt sich nicht aus einer Einstellung ableiten, die
erst zur Laufzeit gelesen wird. Ohne die Freigabe erscheint in einem Shop mit
scharf gestellter CSP kein Prüffenster, und die Ursache steht nur in der
Browser-Konsole.

---

## Beleg an der Bestellung

Ein Händler kauft eine Altersprüfung wegen der Nachweispflicht. Steht an der
Bestellung nichts, kann er bei einem Testkauf oder einer Beanstandung nicht
belegen, dass **diese** Bestellung geprüft war.

Geschrieben wird beides:

* **Ein Eintrag im Bestellverlauf** — sichtbar unter *Verkäufe → Bestellungen →
  <Bestellung> → Kommentare*. Das ist der Teil, den ein Sachbearbeiter
  tatsächlich sieht.
* **Fünf Spalten in `sales_order`** für Auswertungen und Exporte:
  `edelverify_verified` (`ja` / `nicht-noetig`), `edelverify_checked_at`,
  `edelverify_min_age`, `edelverify_mode`, `edelverify_ref`.

  `edelverify_min_age` trägt die **tatsächlich durchgesetzte** Grenze — `16` bei
  einem Bier-Warenkorb, `18` bei Spirituosen. Nicht pauschal 18: Genau diese
  Zahl ist im Streitfall der Beleg. Lautete der vorgelegte Nachweis auf eine
  höhere Grenze, steht das zusätzlich im Verlaufseintrag.

Nie geschrieben werden: der Nachweis selbst, ein Geburtsdatum, ein Name.
`edelverify_ref` trägt acht Zeichen der Prüfkennung — genug, um die Prüfung im
EdelVerify-Admin wiederzufinden, zu wenig, um damit einen Nachweis abzuholen
oder eine Person zu bestimmen.

**Was das Modul bewusst nicht mitbringt:** keine Spalte in der Bestellliste und
keinen eigenen Kasten im Bestellformular. Beides hiesse, in
`sales_order_grid` zu schreiben und `sales_order_view.xml` umzubauen — viel
Fläche für einen Beleg, der im Verlauf ohnehin steht. Wer die Spalte in der
Liste braucht, findet in `PRUEFPLAN.md` den Punkt dazu.

---

## Themes

### Luma

Läuft ohne Zutun. Das Modul hängt sich in `checkout_cart_index` und
`checkout_index_index` in den `content`-Container.

Das eigene JavaScript des Moduls ist absichtlich winzig und benutzt **kein**
RequireJS und **kein** Knockout: zwei Ereignis-Zuhörer in reinem
Browser-JavaScript. gate.js selbst baut sein Fenster in einen Shadow DOM und ist
von der Theme-Welt getrennt.

Die Farben liegen als CSS-Variablen auf `.edelverify-banner`. Ein Theme braucht
zum Umfärben eine Zeile:

```css
.edelverify-banner { --ev-knopf: #1E3B32; --ev-grund: #FFFDF9; }
```

### Hyvä

Das ViewModel `EdelByte\EdelVerify\ViewModel\Gate` ist theme-unabhängig und
lässt sich unverändert weiterverwenden. Nötig sind:

1. Ein Layout-XML im Hyvä-Theme, das denselben Block mit einem eigenen Template
   einhängt (Hyvä benutzt andere Container-Namen als Luma).
2. Ein Template, das den Knopf mit `data-edelverify` ausgibt und das Script-Tag
   setzt. Das mitgelieferte `gate.phtml` lässt sich fast unverändert
   übernehmen — es braucht nur andere Klassennamen (Tailwind statt der
   mitgelieferten CSS-Datei).
3. Kein Alpine-Code. Die zwei Zuhörer im Template reichen; `x-data` ist hier
   nicht nötig.

Die CSS-Datei `view/frontend/web/css/edelverify.css` sollte ein Hyvä-Theme
**nicht** laden — sie würde nur mit dem Tailwind-Build kollidieren.

### PWA Studio / entkoppelte Frontends

**Der Riegel greift, der Weg zum Nachweis nicht.** Das ist Absicht in der
sicheren Richtung, aber ein Frontend-Team muss zwei Dinge nachliefern:

1. **Das Fenster.** `gate.js` in die React-Anwendung laden und den Erfolg
   abgreifen (Ereignis `edelverify:verified` am `document`).
2. **Einen Weg, den Nachweis abzulegen.** `/edelverify/confirm/index` setzt eine
   PHP-Sitzung voraus — die gibt es in einem token-basierten Frontend nicht.
   Nötig ist eine eigene Schnittstelle, die statt der Sitzung den **Warenkorb**
   als Ablage benutzt:
   * eine Spalte `edelverify_proof` an der Tabelle `quote` (`etc/db_schema.xml`,
     genau wie es Magento_Persistent mit `is_persistent` macht),
   * ein `Magento\Framework\Webapi`-Endpunkt, der `cart_id` und
     `verification_id` entgegennimmt,
   * `ProofStore` liest dann den Warenkorb statt der Sitzung.

   Das ist überschaubar, aber es ist Arbeit, und dieses Modul erledigt sie
   nicht. Solange sie nicht erledigt ist, findet der Riegel in einem
   token-basierten Checkout **nie** einen Nachweis und weist **jede**
   betroffene Bestellung ab.

---

## Was ehrlicherweise ungeprüft ist

Dieses Modul ist gegen den **Quellbaum** von Magento 2.4.7-p3 geschrieben: Jede
Klasse, jede Methode, jeder Ereignisname und jeder Schemaverweis wurde dort
nachgeschlagen. Alle XML-Dateien sind gegen die echten XSD-Dateien aus dem
Quellbaum validiert, alle PHP-Dateien mit `php -l` geprüft.

**Es lief noch nie in einer laufenden Magento-Instanz.** Nicht geprüft sind
deshalb unter anderem:

* ob `setup:di:compile` sauber durchläuft,
* ob der Plugin-Punkt in jeder Zahlungsart wirklich greift,
* ob die Layout-Einhängung in jedem Theme an der gedachten Stelle landet,
* ob die Sitzung in einem REST-Kassenaufruf tatsächlich verfügbar ist,
* ob die neuen Spalten in `sales_order` ohne Konflikt angelegt werden.

`PRUEFPLAN.md` listet jeden dieser Punkte einzeln auf, mit dem Befehl, mit dem
er sich in fünf Minuten klären lässt. Wer dieses Modul produktiv einsetzt,
sollte diesen Plan zuerst abarbeiten.

---

## Deinstallation

```bash
bin/magento module:disable EdelByte_EdelVerify
composer remove edelbyte/module-edelverify   # oder rm -rf app/code/EdelByte
bin/magento setup:upgrade
```

`setup:upgrade` entfernt die fünf Spalten aus `sales_order` wieder — dafür ist
`etc/db_schema_whitelist.json` da. **Der Beleg an bestehenden Bestellungen geht
damit verloren.** Wer ihn aufbewahren muss, exportiert vorher:

```sql
SELECT increment_id, edelverify_verified, edelverify_checked_at,
       edelverify_min_age, edelverify_mode, edelverify_ref
FROM   sales_order
WHERE  edelverify_verified IS NOT NULL;
```

Die Einträge im Bestellverlauf bleiben in jedem Fall erhalten — sie liegen in
`sales_order_status_history` und gehören nicht diesem Modul.

---

## Übersetzungen

`i18n/de_CH.csv`, `fr_CH.csv`, `it_CH.csv`, `en_US.csv`.

Abweichend von der Magento-Gewohnheit sind die **Quelltexte deutsch**, nicht
englisch: Das Modul richtet sich an Schweizer Händler, und ein deutscher
Quelltext ist hier der, den am ehesten jemand liest. `en_US.csv` ist damit eine
Übersetzung wie jede andere.

---

## Lizenz und Kontakt

Copyright © EdelByte — https://edelbyte.ch
Dienst und Schlüssel: https://verify.edelbyte.ch
