# Prüfplan

Was sich ohne laufende Magento-Instanz **nicht** klären lässt — je mit dem
Befehl, mit dem es sich klären lässt.

Das Modul ist vollständig gegen den Quellbaum von Magento 2.4.7-p3 geschrieben.
Alle XML-Dateien sind gegen die echten XSD-Dateien validiert, alle PHP-Dateien
mit `php -l` geprüft. Beides sagt nichts darüber, ob es **läuft**.

Reihenfolge ist Absicht: 1–4 sind Abbruchkriterien, 5–12 sind Verhalten, 13–16
sind Feinheiten.

---

## Vorbereitung

```bash
cd <magento-root>
bin/magento deploy:mode:set developer
cp -r <dieses-verzeichnis>/../../EdelByte app/code/
bin/magento module:enable EdelByte_EdelVerify
```

---

## 1 — Lässt sich das Modul überhaupt kompilieren?

**Unklar:** Ob der Objektmanager alle Abhängigkeiten auflösen kann. Konstruktoren
mit `readonly` in *promoted properties* sind PHP-8.1-Syntax; Magentos
Code-Generator (`Interceptor`, `Proxy`, `Factory`) kommt damit erfahrungsgemäss
zurecht, aber «erfahrungsgemäss» ist kein Beleg.

```bash
bin/magento setup:di:compile
```

**Erwartet:** Fehlerfrei. Insbesondere muss
`generated/code/EdelByte/EdelVerify/…` entstehen und
`generated/code/Magento/Quote/Model/QuoteManagement/Interceptor.php` unser
Plugin enthalten:

```bash
grep -c "edelbyte_edelverify" generated/metadata/*.php
```

**Wenn es scheitert:** Zuerst `readonly` aus den Konstruktoren nehmen und erneut
kompilieren — das grenzt Syntax gegen Verdrahtung ab.

---

## 2 — Greift das Plugin am erwarteten Punkt?

**Unklar:** Ob Magento ein Plugin, das auf `Magento\Quote\Api\CartManagementInterface`
angemeldet ist, tatsächlich auf `Magento\Quote\Model\QuoteManagement` überträgt.
Der Kern macht das in `app/code/Magento/Checkout/etc/webapi_rest/di.xml`
(GuestCartManagementInterface), also müsste es tragen — nachgewiesen ist es
damit aber nur für den `webapi_rest`-Bereich, nicht global.

```bash
bin/magento dev:di:info 'Magento\Quote\Api\CartManagementInterface'
```

**Erwartet:** Unter *Plugins* steht
`EdelByte\EdelVerify\Plugin\BlockOrderWithoutProof` mit `beforePlaceOrder`.

**Wenn es fehlt:** Die Anmeldung von `etc/di.xml` nach
`etc/frontend/di.xml` **und** `etc/webapi_rest/di.xml` **und**
`etc/graphql/di.xml` duplizieren. Der Riegel muss in allen dreien stehen — in
nur einem wäre er keiner.

---

## 3 — Werden die Spalten in `sales_order` angelegt?

**Unklar:** Ob `sales_order` in der jeweiligen Installation überhaupt
schreibbar ist. Bei aufgeteilten Datenbanken (`split database`, Adobe Commerce)
liegt `sales` auf einer eigenen Verbindung; `resource="sales"` in
`etc/db_schema.xml` trägt dem Rechnung, ist hier aber nicht erprobt.

```bash
bin/magento setup:upgrade
bin/magento setup:db:status
mysql -e "SHOW COLUMNS FROM sales_order LIKE 'edelverify%'" <db>
```

**Erwartet:** Fünf Zeilen.

---

## 4 — Ist der geheime Schlüssel wirklich verschlüsselt?

**Unklar:** Ob `type="obscure"` + `Magento\Config\Model\Config\Backend\Encrypted`
in dieser Kombination greift. Der Kern nutzt sie (Magento_NewRelicReporting,
Magento_Usps, Magento_Paypal), aber nur der Blick in die Datenbank beweist es.

Schlüssel im Backend eintragen, dann:

```bash
mysql -e "SELECT value FROM core_config_data WHERE path='edelverify/general/secret_key'" <db>
```

**Erwartet:** Etwas wie `0:3:abc…` — **nicht** `sk_live_…`.

Und die Gegenprobe, dass das Entschlüsseln beim Lesen klappt:

```bash
bin/magento config:show edelverify/general/secret_key
```

**Erwartet:** Magentos `ValueProcessor` (der `Encrypted` kennt) gibt hier
`******` oder den Klartext aus — in keinem Fall den Geheimtext.

**Wenn im Klartext gespeichert:** Sofort stoppen. Ein Klartext-Schlüssel in
`core_config_data` landet in jedem Datenbank-Backup.

---

## 5 — Blockiert der Riegel wirklich?

**Unklar:** Ob `CouldNotSaveException` in jeder Kasse als lesbare Meldung
ankommt oder irgendwo zu einem 500er wird.

Vorbereitung: Prüfung aktiv, beide Schlüssel gesetzt, Kategorie eingetragen,
altersbeschränkter Artikel im Warenkorb, **keine** Prüfung durchgeführt.

Dann in der Luma-Kasse auf «Bestellung aufgeben».

**Erwartet:** Die Meldung «Bitte bestätigen Sie zuerst Ihr Alter mit der E-ID …»
erscheint, es entsteht **keine** Bestellung.

```bash
mysql -e "SELECT COUNT(*) FROM sales_order WHERE created_at > NOW() - INTERVAL 5 MINUTE" <db>
```

**Erwartet:** 0.

---

## 6 — Blockiert er auch an der REST-Schnittstelle?

Das ist der Punkt, an dem sich entscheidet, ob der gewählte Plugin-Ort etwas
taugt. Ein Frontend-Riegel liesse sich hier umgehen.

```bash
TOKEN=$(curl -s -X POST '<base-url>/rest/V1/integration/customer/token' \
  -H 'Content-Type: application/json' \
  -d '{"username":"kunde@example.ch","password":"…"}' | tr -d '"')

curl -i -X PUT '<base-url>/rest/V1/carts/mine/order' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"paymentMethod":{"method":"checkmo"}}'
```

**Erwartet:** HTTP 400 mit unserer Meldung im Rumpf. **Nicht** 200 mit einer
Bestellnummer.

---

## 7 — Und in der Gastkasse?

Die Gastkasse geht über `GuestCartManagementInterface`, das laut Quelltext an
`CartManagementInterface` weiterreicht. Das ist belegt, aber Weiterreichen und
Plugin-Auslösung sind zwei verschiedene Dinge — ein Plugin auf dem inneren
Interface feuert nur, wenn der Aufruf wirklich durch den Objektmanager geht und
nicht an einer Instanz vorbei.

```bash
CART=$(curl -s -X POST '<base-url>/rest/V1/guest-carts' | tr -d '"')
# … Artikel und Adressen setzen …
curl -i -X PUT "<base-url>/rest/V1/guest-carts/$CART/order" \
  -H 'Content-Type: application/json' \
  -d '{"paymentMethod":{"method":"checkmo"}}'
```

**Erwartet:** HTTP 400 mit unserer Meldung.

**Wenn 200:** Ein zweites Plugin auf
`Magento\Quote\Api\GuestCartManagementInterface` nachrüsten, das die maskierte
Kennung über `QuoteIdMaskFactory` auflöst — genau wie
`Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforePlaceOrder`.

---

## 8 — Kommt der Nachweis in der Sitzung an?

**Unklar (der grösste Punkt in dieser Liste):** Ob
`Magento\Checkout\Model\Session` in einem REST-Kassenaufruf dieselbe Sitzung
sieht wie der Frontend-Controller, der den Nachweis abgelegt hat. Die
Luma-Kasse schickt ihren Bestellabschluss als AJAX-Aufruf an
`rest/V1/carts/mine/payment-information` — also in den Bereich `webapi_rest`.
Dass dort PHP-Sitzungen grundsätzlich funktionieren, ist belegt
(`Magento\Customer\Model\Authorization\CustomerSessionUserContext` liest die
Kundensitzung). Dass die **Checkout**-Sitzung dort dieselben Daten trägt, ist
es nicht.

Nach einer erfolgreichen Prüfung, vor dem Bestellabschluss:

```bash
tail -f var/log/debug.log &
# in ProofStore::getVerifiedState() voruebergehend ergaenzen:
#   $this->logger->debug('EV proof', ['len' => strlen($proof), 'area' => …]);
```

**Erwartet:** Beim Aufruf von `payment-information` steht dieselbe Länge im Log
wie beim Aufruf von `/edelverify/confirm/index`.

**Wenn der Nachweis dort fehlt:** Das ist kein Sicherheitsproblem — die
Bestellung wird abgewiesen, also fail-closed —, aber es macht das Modul
unbrauchbar. Dann muss `ProofStore` auf den **Warenkorb** umgestellt werden:
Spalte `edelverify_proof` an `quote` (`etc/db_schema.xml`, Vorbild
Magento_Persistent mit `is_persistent`), Schreiben über
`CartRepositoryInterface::save()`, Lesen über `$quote->getData()`. Der Skizze in
der README (Abschnitt *PWA Studio*) folgen.

---

## 9 — Wird der Form Key angenommen?

**Unklar:** Ob `Magento\Framework\Data\Form\FormKey\Validator::validate()` den
Schlüssel aus dem Abfrageteil der Adresse liest. Der Quelltext sagt
`$request->getParam('form_key')`, und `getParam()` bedient laut Laminas sowohl
Abfrageteil als auch Formularfelder — nachweisbar ist das aber nur am
laufenden System (das `vendor/`-Verzeichnis im geprüften Quellbaum ist leer,
Laminas liess sich also nicht direkt nachschlagen).

```bash
curl -i -X POST '<base-url>/edelverify/confirm/index?form_key=<aus-dem-Cookie>' \
  -H 'Content-Type: application/json' \
  -b 'PHPSESSID=…; form_key=…' \
  -d '{"verification_id":"erfunden"}'
```

**Erwartet:** HTTP 502 mit `{"ok":false,"reason":"upstream_unavailable"}` —
also *durch* die Form-Key-Prüfung hindurch bis zum Aufruf des Dienstes.

Und die Gegenprobe **ohne** `?form_key=`:

**Erwartet:** HTTP 403 mit `{"ok":false,"reason":"invalid_form_key"}`.
**Nicht** eine 302-Umleitung.

**Wenn 302 kommt:** `createCsrfValidationException()` wird nicht aufgerufen —
dann prüfen, ob `CsrfAwareActionInterface` an einer Klasse greift, die
`Magento\Framework\App\Action\Action` **nicht** erweitert.

---

## 10 — Erscheint der Hinweis, und an der richtigen Stelle?

**Unklar:** Ob `before="-"` im `content`-Container den Block wirklich über die
Kasse setzt. Bei `checkout_index_index` steht dort ein Knockout-Baum, den das
Theme unter Umständen absolut positioniert.

```bash
bin/magento cache:flush
```

Dann Warenkorb und Kasse im Browser ansehen.

**Erwartet:** Der Streifen steht oben, der Knopf öffnet das Prüffenster.

```bash
bin/magento dev:template-hints:enable
```

zeigt, in welchem Container er tatsächlich gelandet ist.

---

## 11 — Kommt gate.js an der CSP vorbei?

**Unklar:** Ob `etc/csp_whitelist.xml` in dieser Fassung von Magento_Csp
gelesen wird, und ob der inline gerenderte Script-Block einen Nonce bekommt.

Kasse öffnen, Browser-Konsole ansehen.

**Erwartet:** Keine `Content Security Policy`-Meldungen.

```bash
curl -sI '<base-url>/checkout/cart/' | grep -i content-security-policy
```

**Erwartet:** `verify.edelbyte.ch` steht in `script-src`, `connect-src` und
`img-src`.

---

## 12 — Landet der Beleg an der Bestellung?

Eine vollständige Bestellung mit bestandener Prüfung durchführen.

```bash
mysql -e "SELECT increment_id, edelverify_verified, edelverify_checked_at,
                 edelverify_min_age, edelverify_mode, edelverify_ref
          FROM sales_order ORDER BY entity_id DESC LIMIT 1" <db>

mysql -e "SELECT comment FROM sales_order_status_history
          ORDER BY entity_id DESC LIMIT 3" <db>
```

**Erwartet:** `ja`, ein Zeitstempel, die **durchgesetzte Grenze**, den Modus,
acht Zeichen Kennung — und einen Verlaufseintrag «Alter über EdelVerify
bestätigt (ab 18) …».

Bei einem Warenkorb, der nur Ware ab 16 enthält, muss dort `16` stehen, nicht
`18`. Steht überall pauschal `18`, hält der Beleg die falsche Grenze fest — dann
läuft eine ältere Fassung des Moduls.

**Unklar dabei:** Ob `sales_order_place_after` bei **jeder** Zahlungsart feuert.
Bei Weiterleitungs-Zahlungsarten (PayPal Express, Saferpay, Datatrans) entsteht
die Bestellung an anderer Stelle. Diesen Punkt für jede eingesetzte Zahlungsart
einzeln wiederholen.

---

## 13 — Bleibt der Nachweis nach der Bestellung liegen?

`ClearProofAfterOrder` hängt an `checkout_submit_all_after`. Dieses Ereignis
feuert laut Quelltext in `Magento\Quote\Model\QuoteManagement` (Zeile 473) — aber
**nicht** in jedem Weg zur Bestellung.

Nach einer Bestellung sofort eine zweite mit demselben Browser versuchen.

**Erwartet:** Der Hinweis erscheint erneut, die Prüfung wird verlangt.

**Wenn nicht:** Zusätzlich an `sales_model_service_quote_submit_success`
anmelden. Das feuert in derselben Methode, aber früher, und ist der breitere
Aufhänger.

---

## 14 — Verhält sich der Riegel bei einer Störung richtig?

Der wichtigste Punkt der ganzen Liste, und der, den man am ehesten vergisst.

```bash
# Den Dienst unerreichbar machen — nicht abschalten, sondern ins Leere zeigen:
bin/magento config:set edelverify/general/base_url https://127.0.0.1:1
bin/magento cache:flush
```

Dann bestellen.

**Erwartet:** Bestellung **abgewiesen**. Nicht durchgelassen, nicht mit einer
Warnung durchgelassen.

Dasselbe mit einem falschen geheimen Schlüssel und mit einem geleerten
Schlüssel wiederholen.

```bash
bin/magento config:set edelverify/general/base_url https://verify.edelbyte.ch
```

---

## 15 — Trifft die Kategorie-Erkennung?

**Unklar:** Ob `$item->getProduct()->getCategoryIds()` auf einem Warenkorbposten
die Kategorien liefert. Der Produkt-Datensatz am Warenkorbposten ist unter
Umständen mit eingeschränktem Attributsatz geladen; `getCategoryIds()` geht
zwar an das Resource-Modell, aber bei konfigurierbaren Produkten hängen die
Kategorien am Elternteil, nicht an der Variante.

Vier Fälle prüfen:

| Fall | Erwartung |
|---|---|
| Einfaches Produkt in betroffener Kategorie | Prüfung verlangt |
| Einfaches Produkt in einer anderen Kategorie | keine Prüfung |
| Konfigurierbares Produkt, Kategorie am Elternteil | Prüfung verlangt |
| Bündel / Gruppe mit betroffenem Bestandteil | **offen** — hier ist am
  ehesten mit einer Lücke zu rechnen |

Der Code läuft über `getAllItems()` (Elternteil **und** Varianten), was den
dritten Fall abdecken sollte. Der vierte ist ungeklärt.

---

## 15b — 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 — für ihn ist alles Bisherige bereits die vollständige Prüfung.

Vorbereitung: `bin/magento config:set edelverify/general/category_ids "17:16,12:18"`
(17 = Bier, 12 = Spirituosen), Vorgabe auf 18 lassen.

| Fall | Erwartet |
|---|---|
| nur Bier im Warenkorb | 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 | Bestellung wird **wieder** abgewiesen |
| danach Gin entfernen | Bestellung geht durch, **keine** neue Prüfung |
| `category_ids` auf `17:17` | gilt als 18, nicht als 16 |

**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 180 Sekunden**
dazu (`ProofStore::CACHE_TTL`) — genau in diesem Fenster glaubt das Modul
seiner letzten Antwort. Bliebe die Bestellung möglich, wäre der Speicher nicht
an die Grenze gebunden, und das Loch wäre drei Minuten breit.

Gegen die Schnittstelle, ohne Magento:

```bash
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"
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_18` auch dann `false`, wenn die Person alt genug ist. Ein Modul, das nur
> dieses Feld liest, weist jede bestandene 16er-Prüfung ab und merkt es nie,
> weil im 18er-Betrieb beide Felder dasselbe sagen. Dass dieses Modul `age_ok`
> liest, ist belegt: `python3 scripts/pakete-pruefen.py`, Abschnitt J.

---

## 16 — Kleinigkeiten

* **Mehrfachversand** (`Magento_Multishipping`) geht über
  `Magento\Multishipping\Model\Checkout\Type\Multishipping`, nicht über
  `CartManagementInterface::placeOrder()`. **Der Riegel greift dort
  vermutlich nicht.** Prüfen, und falls bestätigt, entweder ein zweites Plugin
  nachrüsten oder den Mehrfachversand für betroffene Warenkörbe abschalten.
* **Admin-Bestellungen** (*Verkäufe → Bestellungen → Neu*) gehen über
  `Magento\Sales\Model\AdminOrder\Create`. Der Riegel greift dort nicht — das
  ist vertretbar (ein Mitarbeiter prüft am Telefon selbst), sollte aber bewusst
  entschieden und dem Händler gesagt werden.
* **Spalte in der Bestellliste.** Nicht mitgeliefert. Wer sie will, braucht
  eine `sales_order_grid`-Spalte plus einen Eintrag in
  `view/adminhtml/ui_component/sales_order_grid.xml`. Vorbild:
  `Magento\Sales\etc\di.xml`, Abschnitt `sales_order_grid_data_source`.
* **Zwischenspeicher-Laufzeit.** `ProofStore::CACHE_TTL` steht auf 180 Sekunden.
  Ob das der richtige Kompromiss zwischen «zurückgezogener Nachweis fällt schnell
  auf» und «nicht bei jeder Seite über das Netz» ist, zeigt erst der Betrieb.
* **Verbindungstest im Backend.** Die WooCommerce-Fassung hat einen Knopf, der
  in einem Durchgang Erreichbarkeit, geheimen Schlüssel und Herkunftsprüfung
  testet. Dieses Modul hat ihn **nicht** — `EdelVerifyClient::createVerification()`
  ist dafür bereits vorhanden, es fehlt der Admin-Controller und der Block.
  Das ist die erste Ergänzung, die sich lohnt: «Es geht nicht» hat bei einer
  Altersprüfung fast immer eine dieser drei Ursachen.
