# Prüfplan — EdelVerify für WooCommerce

Diese Datei beantwortet eine einzige Frage: **Woran erkennen Sie, dass der
Riegel wirklich hält?**

Die Einbauanleitung steht in `readme.txt`. Sie führt bis dahin, dass alles
eingerichtet *aussieht*. Das genügt bei einer Altersprüfung nicht — eine
abgeschaltete Schranke sieht auf der Bestellseite genauso aus wie eine
funktionierende, solange niemand das Alter bestätigt hat und trotzdem bestellt.

Wer die Punkte 1 bis 7 abgearbeitet hat, hat es gemessen und muss es nicht mehr
glauben. Rechnen Sie mit einer knappen Stunde.

> **Neu: die Altersgrenze ist einstellbar.** 16 oder 18, je Kategorie, mit einer
> Vorgabe für den ganzen Shop. Ein Shop, der nichts umstellt, prüft weiterhin
> alles gegen 18 — Punkt 5 misst genau das nach. Wer Bier ab 16 und Spirituosen
> ab 18 verkauft, arbeitet zusätzlich Punkt 6 ab; dort steckt der einzige
> wirklich neue Fehler, den man machen kann.

---

## 0. Was Sie brauchen

* Einen Shop, in dem eine **Testbestellung nicht stört** — ein Staging-System
  oder ein frisch aufgesetzter Shop. Punkt 3 legt Bestellungen an.
* Beide Schlüssel aus dem EdelVerify-Admin unter «Shops». Nehmen Sie die
  **Testschlüssel** (`pk_test_…`, `sk_test_…`). Damit lässt sich jeder Ausgang
  auf Bestellung herbeiführen, auch der, den Sie sonst nie zu sehen bekämen.
* `curl` und eine Kommandozeile. Der halbe Plan läuft ohne Browser, und das ist
  Absicht: Wer nur die Oberfläche prüft, prüft genau den Weg, den ein Umgeher
  nicht nimmt.

> **Der geheime Schlüssel gehört nicht in eine Chatnachricht, kein Ticket und
> keinen Screenshot.** Er ist der Riegel; wer ihn hat, stellt sich Nachweise
> selbst aus. Fällt er hinaus, ziehen Sie ihn im EdelVerify-Admin zurück und
> tragen einen neuen ein — das dauert eine Minute und ist immer die richtige
> Entscheidung.

---

## 1. Installation

```
Plugins → Installieren → Plugin hochladen → edelverify-plugin.zip → aktivieren
```

| Prüfen | Erwartet |
|---|---|
| Plugin-Liste | «EdelVerify — Altersprüfung mit der E-ID», aktiv |
| Oben im Backend | Gelber Hinweis «Schlüssel fehlen» |
| `WooCommerce → Einstellungen` | Reiter **EdelVerify** ist da |

Der gelbe Hinweis ist kein Schönheitsfehler, sondern die erste bestandene
Prüfung: Das Plugin sagt von sich aus, dass es noch nicht arbeiten kann.

---

## 2. Konfiguration und Verbindungstest

Unter `WooCommerce → Einstellungen → EdelVerify`:

| Feld | Wert |
|---|---|
| Adresse des Dienstes | `https://verify.edelbyte.ch` |
| Öffentlicher Schlüssel | `pk_test_…` |
| Geheimer Schlüssel | `sk_test_…` |
| Altersgrenze (Vorgabe) | `ab 18 Jahren` — gilt, wo keine Kategorie etwas anderes sagt |
| Betroffene Kategorien | die Slugs, z. B. `vape,tabak,spirituosen` |

**Die Grenze je Kategorie** hängen Sie mit Doppelpunkt an den Slug:

```
bier:16,wein:16,spirituosen:18,vape
```

Ein Slug ohne Zusatz bekommt die Vorgabe. Damit bleibt jede Einstellung aus der
Zeit vor der zweiten Schwelle Zeichen für Zeichen gültig und verhält sich
unverändert. Liegt Gemischtes im Warenkorb, gilt die **strengste** Grenze.

Ein Zusatz, den der Dienst nicht kennt (`:17`, `:achtzehn`), wird auf 18
angehoben und ins Log geschrieben — nicht auf 16 gesenkt. Prüfen lässt sich das
in Punkt 6.

Speichern, dann **Verbindung prüfen**. Vier Zeilen müssen grün sein:
Erreichbarkeit, beide Schlüssel, Kategorien — und die Grenze, die dort im
Klartext steht. Lesen Sie sie: Sie ist die eine Zahl, die im Streitfall zählt,
und ein Auswahlfeld liest niemand nach.

**Die Herkunft ist der häufigste Stolperstein.** Der öffentliche Schlüssel darf
nur von Adressen aus Prüfungen starten, die im EdelVerify-Admin hinterlegt
sind. Läuft der Shop unter mehreren Adressen — mit und ohne `www`, dazu ein
Staging-System —, gehören **alle** dorthin. Sonst schlägt die Prüfung genau auf
einer davon fehl, und zwar meist auf der, die Sie nicht getestet haben.

Zum Gegenprobieren: Ändern Sie ein Zeichen im geheimen Schlüssel und drücken
Sie noch einmal. Der Test muss rot werden. Wird er grün, prüft er nichts.

---

## 3. Der Riegel — ohne jede Altersbestätigung

Der wichtigste Abschnitt. Geprüft wird gegen die **Store-API**, nicht über die
Oberfläche: Das ist derselbe Weg, den der Block-Checkout intern nimmt, und
gleichzeitig der Weg, den jemand nähme, der die Schranke umgehen will.

Für einen Shop unter `https://ihr-shop.example` liegt fertig bei:

```
./scripts/pruefe-shop.sh https://ihr-shop.example
```

Es misst drei Behauptungen nach:

1. Ein Artikel **ohne** Altersgrenze lässt sich bestellen.
2. Ein altersbeschränkter Artikel wird **abgewiesen**.
3. Die Abweisung kommt aus EdelVerify (`edelverify_age_required`) und nicht
   zufällig von woanders — einer fehlenden Versandart etwa.

> Das Skript **legt Bestellungen an** (Zahlungsart «Nachnahme»). Führen Sie es
> nicht gegen einen Shop aus, in dem echte Bestellungen liegen. Es geht auch
> nicht von den Slugs Ihres Shops aus, sondern von denen des Demo-Shops
> (`zubehoer`, `liquids`) — für den eigenen Shop die beiden Stellen im Skript
> anpassen.

**Der Stand für entkoppelte Oberflächen.** Wer eine eigene Storefront, eine App
oder eine kopflose Oberfläche baut, muss die Schranke anzeigen können, bevor die
Kundschaft die Adresse eingetippt hat. Dafür hängt das Plugin drei Angaben an
die Store-API:

```bash
curl -s https://ihr-shop.example/wp-json/wc/store/v1/cart -b kekse.txt \
  | python3 -c "import sys,json; print(json.load(sys.stdin).get('extensions',{}).get('edelverify'))"
```

| Warenkorb | Erwartet |
|---|---|
| leer | `{'pruefung_noetig': False, 'grenze': <Vorgabe>, 'erfuellt': True}` |
| Artikel ohne Altersgrenze | `pruefung_noetig: False` |
| altersbeschränkter Artikel, ungeprüft | `pruefung_noetig: True, erfuellt: False` |
| derselbe, nach bestandener Prüfung | `pruefung_noetig: True, erfuellt: True` |
| nur Bier (`bier:16`) | `grenze: 16` |
| Bier **und** Gin (`spirituosen:18`) | `grenze: 18` — die strengste gewinnt |

`grenze` ist seit der zweiten Schwelle die **echte** Grenze dieses Warenkorbs,
nicht mehr pauschal 18. Bei leerem Warenkorb steht dort die Vorgabe des Shops:
Ein Client soll wissen, womit er zu rechnen hat, bevor die erste Flasche im Korb
liegt.

Kommt `None` zurück, ist das Plugin älter als diese Angabe oder der
Block-Support fehlt. Steht dort ein `av_…` oder eine Kennung, melden Sie es
uns — dort gehört nichts hin ausser diesen drei Werten.

**Beide Kassenwege einzeln prüfen.** WooCommerce liefert heute den
Block-Checkout aus, viele Shops laufen noch klassisch, und das Plugin hängt an
zwei verschiedenen Haken:

| Weg | Haken im Plugin | Wie prüfen |
|---|---|---|
| Block-Checkout / Store-API | `woocommerce_store_api_cart_errors` | das Skript oben |
| Klassischer Checkout | `woocommerce_checkout_process` | Seite mit `[woocommerce_checkout]` aufrufen und von Hand bestellen |

Beide müssen abweisen. Greift nur einer, hat der Shop eine Kasse ohne Schloss —
und es fällt niemandem auf, weil die andere hält.

---

## 4. Eine vollständige Prüfung, von Anfang bis Ende

Testschlüssel sprechen den Prüfdienst des Bundes gar nicht erst an. Sie stehen
vier Sekunden auf `PENDING` — damit die Abfrageschleife wirklich durchläuft —
und nehmen dann den Ausgang, den Sie bestellt haben.

1. Altersbeschränkten Artikel in den Warenkorb legen.
2. Zur Kasse. Es erscheint der Hinweis mit dem Knopf «Alter bestätigen».
3. Klicken. Das Fenster geht auf, mit QR-Code.
4. Nach wenigen Sekunden wird es grün.
5. **Jetzt nicht neu laden**, sondern warten, bis die Seite es von sich aus tut.

> **Punkt 5 ist der Fehler, den fast jede Einbindung einmal macht.** «Grün»
> heisst: die Prüfung ist bestanden. Es heisst nicht, dass Ihr Shop davon weiss
> — die Rückmeldung an `/wp-json/edelverify/v1/confirm` läuft in diesem
> Augenblick erst los. Wer hier neu lädt, bricht sie ab. Der Nachweis wird nur
> einmal ausgeliefert und ist damit verbrannt: Die Seite kommt mit derselben
> Schranke zurück, und für die Kundschaft sieht es aus, als hätte die Prüfung
> nichts bewirkt.

6. Bestellen. Die Bestellung geht durch.
7. `WooCommerce → Bestellungen`: Die Spalte **EdelVerify** zeigt den Beleg.

Was in der Bestellung stehen muss und was nicht:

| Feld | Inhalt |
|---|---|
| `edelverify_verified` | `1` |
| `edelverify_checked_at` | Zeitpunkt |
| `edelverify_min_age` | die **tatsächlich durchgesetzte** Grenze — `16` bei einem Bier-Warenkorb, `18` bei Gin |
| `edelverify_mode` | `test` |
| `edelverify_check_ref` | acht Zeichen, zum Wiederfinden im EdelVerify-Admin |
| **der Nachweis selbst** | **steht nirgends** — er ist eingelöst, nicht abgelegt |

Steht dort ein `av_…`, ist etwas falsch: Der Nachweis ist ein Inhaberpapier und
gilt dreissig Tage. Wer ihn aus dem Bestellexport abschreibt, kauft damit einen
Monat lang ohne Prüfung.

Steht in `edelverify_min_age` pauschal `18`, obwohl der Warenkorb nur Bier
enthielt, ist der Beleg falsch — dann läuft eine ältere Fassung des Plugins.
Der Beleg muss die Grenze festhalten, gegen die wirklich geprüft wurde; das ist
der ganze Zweck der Zeile.

---

## 5. Der Fall, den Sie am seltensten sehen und am dringendsten können müssen

Ein Shop, der nur den Erfolgsfall geprüft hat, hat die Altersprüfung nicht
geprüft. Mit Testschlüsseln lässt sich jeder Ausgang bestellen — dafür sind sie
da.

Der Ausgang wird beim Start der Prüfung mitgegeben. Am schnellsten von Hand:

```bash
# Prüfung mit dem gewünschten Ausgang beginnen
curl -s -X POST https://verify.edelbyte.ch/api/v1/verifications \
  -H "x-api-key: $SK" -H 'content-type: application/json' \
  -d '{"reference":"pruefplan","test_outcome":"under_18"}'

# vier Sekunden warten, dann den Stand holen
sleep 5
curl -s "https://verify.edelbyte.ch/api/v1/verifications/<id>" -H "x-api-key: $SK"
```

| `test_outcome` | Erwartet | Was der Shop tun muss |
|---|---|---|
| `success` | `SUCCESS`, `age_ok: true`, Nachweis dabei | durchlassen |
| `under_18` | `SUCCESS`, `age_ok: false`, **kein** Nachweis | abweisen |
| `failed` | `FAILED`, `error_code: test_failed` | abweisen |
| `expired` | `EXPIRED` | abweisen, neu anbieten |
| `ueber_18` (Tippfehler) | `400 invalid_test_outcome` | — |

`SUCCESS` heisst «die Prüfung ist abgeschlossen», nicht «alt genug».
Massgeblich ist `age_ok`. Eine Einbindung, die nur auf den Status schaut, lässt
bei `under_18` durch — und merkt es nie, weil dieser Fall im Alltag fast nie
vorkommt.

> **`over_18` ist nicht `age_ok`.** Bei einer Prüfung gegen 16 steht in
> `over_18` auch dann `false`, wenn die Person die Grenze erfüllt — geprüft
> wurde ja gegen 16, nicht gegen 18. Eine Einbindung, die weiterhin nur
> `over_18` liest, weist damit jede bestandene 16er-Prüfung ab. Im
> 18er-Betrieb fällt das nie auf, weil dort beide Felder dasselbe sagen.
> Nachgemessen an der laufenden Anlage: `python3 scripts/api-vertragstest.py`,
> Abschnitt 10.

Im Shop durchspielen: Kasse aufrufen, Prüfung starten, und wenn das Fenster rot
wird, trotzdem bestellen. Die Bestellung muss abgewiesen werden.

---

## 6. 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; Punkt 5 hat für ihn bereits alles gemessen.

Tragen Sie unter «Betroffene Kategorien» ein: `bier:16,spirituosen:18`.

| Warenkorb | Erwartet |
|---|---|
| nur Bier | Streifen sagt «über 16», Prüffenster sagt «Über 16 bestätigt» |
| nur Gin | Streifen sagt «über 18» |
| Bier **und** Gin | Streifen sagt «über 18» — die strengste Grenze gewinnt |
| Bier, geprüft, dann Gin dazulegen | Kasse geht **wieder zu** |

**Die letzte Zeile ist die wichtige.** Sie misst, ob das Plugin ein «ja» für 16
in einen Warenkorb ab 18 hinüberträgt. So führen Sie sie herbei:

1. Nur Bier in den Warenkorb, Prüfung bestehen, Kasse ist offen.
2. **Innerhalb von drei Minuten** eine Flasche Gin dazulegen.
3. Zur Kasse. Sie muss zu sein und nach einer neuen Prüfung verlangen.

Drei Minuten, weil das Plugin ein bestandenes Ergebnis so lange zwischenspeichert.
Bliebe die Kasse offen, wäre der Zwischenspeicher an die Grenze nicht gebunden —
und das Loch wäre exakt drei Minuten breit und für niemanden sichtbar.

4. Danach die Prüfung wiederholen. Jetzt läuft sie gegen 18 und die Kasse öffnet
   wieder.
5. Zum Gegenprobieren die Flasche Gin wieder hinauswerfen: Der Nachweis ab 18
   löst auch den Bier-Warenkorb ein, es ist **keine** neue Prüfung nötig.

Am Bestellbeleg prüfen: `edelverify_min_age` steht bei der Bier-Bestellung auf
`16` und bei der gemischten auf `18`. Steht überall `18`, hält der Beleg die
falsche Grenze fest.

**Ohne Browser**, direkt gegen die Schnittstelle — dasselbe in drei Aufrufen:

```bash
# 16er-Prüfung starten
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
# Stand holen — 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
```

| Aufruf | Erwartet |
|---|---|
| `min_age: 16` beim Start | `201`, Antwort trägt `min_age: 16` |
| `min_age: 17`, `21`, `"18"`, `null`, `0` | `400 invalid_min_age` |
| Start **ohne** `min_age` | `201` mit der Vorgabe Ihres Kontos (ab Werk 18) |
| Nachweis ab 16 → Ware ab 18 | `200` mit `valid: false` |
| Nachweis ab 18 → Ware ab 16 | `200` mit `valid: true` |
| Einlösen mit `min_age: 17` | `200` mit `valid: false` — hier gibt es **kein** 4xx |

---

## 7. Fail-closed — jede Störung endet in einer gesperrten Kasse

Nach jedem Fall die Einstellung wieder zurücksetzen.

| Fall | Wie herbeiführen | Erwartet |
|---|---|---|
| Prüfstelle weg | Adresse des Dienstes auf `https://127.0.0.1:9` | Kasse zu, Antwort nach ≤ 10 s |
| Geheimer Schlüssel falsch | ein Zeichen ändern | Kasse zu |
| Kein geheimer Schlüssel | Feld leeren | Kasse zu |
| Öffentlicher Schlüssel falsch | ein Zeichen ändern | Fenster geht gar nicht erst auf |
| Herkunft nicht hinterlegt | Adresse im EdelVerify-Admin entfernen | `403 origin_not_allowed`, Kasse zu |
| Nachweis verfälscht | in der Sitzung `edelverify_proof` ein Zeichen ändern | Kasse zu |
| Nachweis abgelaufen | im EdelVerify-Admin die Gültigkeit auf 0 Tage stellen | Kasse zu |
| Schlüssel gewechselt | nach bestandener Prüfung `sk_` austauschen | Kasse **sofort** zu |
| Prüfung noch offen | eine `PENDING`-Kennung an `/wp-json/edelverify/v1/confirm` senden | `422 not_verified` |
| Nachweis schon abgeholt | dieselbe Kennung zweimal senden | 2. Mal `409 proof_consumed` |
| Kategorien leer | Feld leeren | **der ganze Shop** wird geprüft, gegen die Vorgabe |
| Unbekannte Grenze | `bier:17` eintragen | gilt als `18`, Zeile im WooCommerce-Log |
| Unbekannter Slug | `bierr:16` eintragen | **ganzer** Warenkorb prüfpflichtig, gegen die strengste eingetragene Grenze |
| Grenze zu schwach | mit 16er-Nachweis Gin bestellen | Kasse zu, auch innerhalb der drei Minuten |
| Plugin aus | deaktivieren | Bestellung geht durch, **kein** Netzaufruf |

Die vorletzte Zeile ist kein Fehler, sondern eine Entscheidung: Ein leeres
Kategorienfeld heisst «alles prüfen», nicht «nichts prüfen». Für einen Laden,
der auch Zubehör ohne Altersgrenze führt, ist das zu weit — deshalb steht es
hier, damit es beim Ausprobieren nicht überrascht.

Die Zeile «Schlüssel gewechselt» prüft etwas Feineres: Das Plugin merkt sich
das Ergebnis kurz zwischen (`edelverify_ok_…`, drei Minuten). Wird der Riegel
erst nach Ablauf dieser Zeit dicht, gilt der Zwischenspeicher für den falschen
Schlüssel — dann muss der Cache-Schlüssel den geheimen Schlüssel einbeziehen.
Für die Altersgrenze gilt dasselbe, und die Zeile «Grenze zu schwach» misst es:
Der Zwischenspeicher unterscheidet nach verlangter Grenze, sonst öffnete ein
«ja» für Bier drei Minuten lang auch den Gin.

---

## 8. Was ohne diesen Plan bereits belegt ist

Damit Sie wissen, wo Sie ansetzen und wo nicht:

| Behauptung | Womit belegt |
|---|---|
| Jeder angemeldete Haken hat eine Methode | `python3 scripts/pakete-pruefen.py woocommerce` |
| Kein Fehlerweg endet in einer Freigabe | `python3 scripts/pakete-pruefen.py`, Abschnitt E |
| `over_18` genügt nirgends allein als Freigabe | dieselbe Prüfung, Abschnitt J |
| Die Grenze reist mit — Einlösen, Zwischenspeicher, Vorgabe 18 | dieselbe Prüfung, Abschnitt K |
| Die Prüfungen J, K und E schlagen überhaupt noch an | dieselbe Prüfung, Abschnitt «Selbstprüfung» |
| Kein geheimer Schlüssel gelangt in den Browser | dieselbe Prüfung, Abschnitt F |
| Alle drei aufgerufenen API-Pfade gibt es wirklich | dieselbe Prüfung, Abschnitt G |
| Die Schnittstelle verhält sich wie beschrieben | `python3 scripts/api-vertragstest.py` (läuft gegen verify.edelbyte.ch) |
| 16 und 18 verhalten sich wie oben beschrieben | dieselbe Prüfung, Abschnitt 10 |
| Der Riegel hält im Demo-Shop | `./scripts/pruefe-shop.sh` gegen `woocommerce.edelbyte.ch` |

Was **nicht** belegt ist und nur dieser Plan klärt: dass es in **Ihrem** Shop
hält — mit Ihrem Theme, Ihren Kategorien, Ihren Zahlungsarten und Ihrem
Kassenweg.

---

## 9. Was heute noch nicht geht

Damit Sie es hier erfahren und nicht nach der Installation:

* **Zwei Schwellen, nicht mehr.** 16 und 18 — das sind die Grenzen, die die
  staatliche E-ID belegt. Eine 21 gibt es nicht, und ein Feld, in das man sie
  eintragen könnte, ohne dass sie wirkt, wäre schlimmer als keines.
* **Keine Webhooks.** Der Stand wird abgefragt, nicht zugestellt. Prüft die
  Kundschaft am Handy, während der Rechner-Tab geschlossen ist, erfährt der Shop
  davon nichts.
* **Ein Nachweis gilt bei einem Shop.** Wer mehrere Shops betreibt, braucht je
  Shop ein eigenes Schlüsselpaar.
* **Noch nicht scharf schalten.** Geprüft wird heute gegen die öffentliche Beta
  des Bundes; dort besitzt nur die Beta-ID einen gültigen Nachweis. Eine richtig
  gebaute Einbindung blockiert im Zweifel — und blockierte derzeit fast jede
  Bestellung. Bauen und prüfen Sie mit Testschlüsseln; am Tag der Umstellung
  tauschen Sie zwei Zeichenfolgen aus, sonst nichts.
