Warenwirtschaft: Review-Fix — AP-32-Label als Base64 übertragen (Connector 1.5.0)

Die Brücke hätte live nicht funktioniert: Connector 1.4.0 sendete die
label_url aus dem per 'deny from all' geschützten DHL-Ordner (403), dazu
SSRF-Risiko durch serverseitigen Abruf einer Client-URL und Timeout-
Mismatch (20 s Download vs. 5 s WordPress-Default). Der Connector liest
die PDF jetzt vom Shop-Dateisystem (realpath-Pinning auf wp-uploads) und
sendet sie Base64; storeWpLabel() validiert und speichert ohne Http::get;
tote Shop-Link-Fallbacks aus den Views entfernt; Connector-Timeout 30 s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-20 10:06:39 +00:00
parent ddd6dde593
commit 1b9b6adc44
11 changed files with 173 additions and 83 deletions

View file

@ -1,52 +1,74 @@
# AP-32 Brückenlösung — DHL-Label aus WooCommerce übernehmen (17.07.2026)
# AP-32 Brückenlösung — DHL-Label aus WooCommerce übernehmen (17.07.2026, Base64 seit 20.07.2026)
## Kontext
Die vollständige DHL-API-Anbindung (AP-32) ist nachgelagert (Kundenentscheid 16.07.2026).
Bis dahin: Das Label wird wie bisher **im WooCommerce-Shop** über das Plugin
**„DHL for WooCommerce"** erstellt; bei der Übertragung der Bestellung ins
Partnercenter kommen **Label-URL und Sendungsnummer mit**.
Partnercenter kommt die **Label-PDF selbst (Base64) plus Sendungsnummer mit**.
Das Partnercenter lädt die Label-PDF **einmalig herunter** und speichert sie im
privaten Storage (`storage/app/wp-labels/`) — Gründe:
> **Änderung 20.07.2026 (Code-Review vor Live-Release, Connector 1.5.0):**
> Ursprünglich (1.4.0) wurde nur die `label_url` übertragen und das Partnercenter
> lud die PDF herunter. Das funktionierte in Produktion **nicht**: Das DHL-Plugin
> schützt den Label-Ordner `wp-content/uploads/woocommerce_dhl_label/` per
> „deny from all" — der Abruf lief immer auf **403**. Außerdem war der
> serverseitige Abruf einer vom Client gelieferten URL ein **SSRF-Risiko** und
> blockierte den `wp/order`-Request bis zu 20 s (WordPress bricht nach 5 s ab
> → Übertragung wäre shopseitig dauerhaft als fehlgeschlagen markiert worden).
> Jetzt liest der Connector die Datei direkt vom Shop-Dateisystem
> (`label_path` aus dem DHL-Meta) und schickt sie **Base64-codiert** mit —
> kein Download, kein SSRF, kein Timeout-Fenster.
1. Die Shop-URL liegt unter `wp-content/uploads/woocommerce_dhl_label/` und ist
**öffentlich erreichbar + erratbar** (Bestellnummer im Dateinamen, Kundenadresse
auf dem Label). Die lokale Kopie wird nur über die eingeloggte Admin-Route
ausgeliefert.
Das Partnercenter speichert die PDF im privaten Storage
(`storage/app/wp-labels/`) — Gründe:
1. Der Shop-Ordner ist zugriffsgeschützt, eine Verlinkung wäre tot; die lokale
Kopie wird nur über die eingeloggte Admin-Route ausgeliefert (Kundenadresse
auf dem Label!).
2. Die Kopie bleibt verfügbar, auch wenn das Label im Shop gelöscht/storniert wird.
## Was das Partnercenter jetzt kann (Laravel-Seite, umgesetzt 17.07.2026)
## Was das Partnercenter jetzt kann (Laravel-Seite, 17.07.2026 / Base64 20.07.2026)
- `POST /api/wp/order` akzeptiert zusätzlich (beides optional):
- `wp_label_url` — öffentliche URL der Label-PDF im Shop
- `POST /api/wp/order` akzeptiert zusätzlich (alles optional):
- `wp_label_pdf` — die Label-PDF **Base64-codiert** (max. 10 MB dekodiert,
Inhalt muss mit `%PDF` beginnen, sonst wird sie verworfen und nur geloggt)
- `wp_label_url` — Original-URL im Shop, **reines Herkunfts-Metadatum**
(wird nie abgerufen und nie verlinkt)
- `wp_tracking_number` — Sendungsnummer (String **oder** Array bei Mehrpaket-Sendung)
- **Neu** `POST /api/wp/label` (Passport-Auth wie die übrigen `wp/*`-Endpoints) —
Label **nachreichen/aktualisieren**, wenn es erst nach der Übertragung erstellt wird:
`{ wp_order_number, wp_label_url, wp_tracking_number }`
`{ wp_order_number, wp_label_pdf, wp_tracking_number }`
- `POST /api/wp/status` liefert je Bestellung zusätzlich `tracking_number` zurück
(für den Tracking-Link an den Endkunden im Shop).
- Anzeige: Abwicklungs-Board + Bestell-Details zeigen „Label"-Download
(lokale Kopie; Fallback: Shop-Link) und die Sendungsnummer.
- Ein fehlgeschlagener Label-Download blockiert die Bestell-Übertragung **nie**
(URL wird gespeichert, Warnung im Laravel-Log).
(nur die lokale Kopie — kein Shop-Link mehr, der Ordner ist zugriffsgeschützt)
und die Sendungsnummer.
- Ein ungültiges/fehlendes Label blockiert die Bestell-Übertragung **nie**
(Sendungsnummer + URL-Metadatum werden trotzdem gespeichert, Warnung im Log).
## Update im Verbindungs-Plugin (WooCommerce-Seite) — umgesetzt 17.07.2026
Das DHL-Plugin speichert an der Bestellung das Meta-Feld
`_pr_shipment_dhl_label_tracking` mit `label_url`, `label_path`, `tracking_number`
(+ ggf. `return_label_url`). Das Update ist im Connector
**`dev/partnercenter-api` (PARTNERCENTER API Connector, Version 1.4.0)** umgesetzt:
**`dev/partnercenter-api` (PARTNERCENTER API Connector, Version 1.5.0)** umgesetzt:
### 1. Label bei der Bestell-Übertragung mitschicken
`public/partnercenter-api/partnercenter-api-requests/order.php`:
- Neuer Helper `compose_label_data()` — liest das DHL-Meta-Feld und liefert
`wp_label_url` + `wp_tracking_number` (String oder Array; leeres Array, wenn
noch kein Label existiert).
- Helper `compose_label_data()` — liest das DHL-Meta-Feld und liefert seit 1.5.0
`wp_label_pdf` (Base64 aus `label_path`, s. u.) + `wp_tracking_number`
(String oder Array) + `wp_label_url` (nur Metadatum); leeres Array, wenn
weder Label noch Sendungsnummer existieren.
- Neuer Helper `read_label_file()` (1.5.0) — liest die Label-PDF vom
Shop-Dateisystem: `realpath` muss **innerhalb von wp-uploads** liegen,
`.pdf`-Endung, Inhalt muss mit `%PDF` beginnen; sonst `false` (dann geht
nur die Sendungsnummer raus).
- `write_order_items()` (baut den `POST wp/order` des „Übertragen"-Buttons)
merged die Label-Daten in den Request-Body.
- `methods.php` `create()`: Request-Timeout 30 s statt WordPress-Default 5 s
(Payload trägt jetzt die Base64-PDF).
### 2. Label automatisch nachreichen (wenn es nach der Übertragung erstellt wird)
@ -61,7 +83,8 @@ Das DHL-Plugin speichert an der Bestellung das Meta-Feld
`public/partnercenter-api/partnercenter-api.php` + Trait-Methode
`write_order_label()` → `POST wp/label` (nutzt die bestehende
Auth-/Request-Infrastruktur des Connectors).
- Eingebunden über `require_once` im Haupt-Plugin-File; Version 1.3.3 → 1.4.0.
- Eingebunden über `require_once` im Haupt-Plugin-File; Version 1.3.3 → 1.4.0 →
**1.5.0 (Base64-Übertragung, 20.07.2026)**.
Hinweise:

View file

@ -276,6 +276,7 @@ Der Warenkorb kommt also **per Knopfdruck** (Fulfillment-Übergabe), nicht autom
| 17.07.2026 | **Kundenentscheid: Rohstoffbestand-Default „Nur aus Herstellerrezepturen"** | `RawMaterialStockController@index`: Default-Scope von `all` auf `recipe` gedreht (`?scope=all` zeigt weiterhin alles); Button-Gruppe entsprechend verlinkt, Hinweise-Doku angepasst. | `RawMaterialStockTest` angepasst (Default filtert, `scope=all` zeigt alle). Volle Suite: **286 grün, 1052 Assertions** |
| 20.07.2026 | **Review-Fix: Stichtags-Schutz unveränderlich (`sale_stock_exempt`)** | Code-Review vor Live-Release: Der AP-13-Stichtags-Schutz hing an `shipped_at`, das `OrderStatusService::setShipped()` bei jedem Wechsel weg von versendet/abgeschlossen **nullt** und beim Zurückwechseln mit `now()` neu stempelt — Storno + erneutes Versenden einer Alt-Bestellung hätte rückwirkend vom heutigen Bestand abgezogen (Verletzung der dokumentierten Garantie, hinweise.md). Jetzt: Migration `2026_07_20_090000` ergänzt `shopping_orders.sale_stock_exempt` (bool) mit **einmaligem Backfill** (`shipped_at` < 16.07.2026 ⇒ exempt) — das Flag wird danach nie verändert; `syncSaleStock()` prüft nur noch das Flag, die Konstante `SALE_STOCK_SINCE` wandert in die Migration. | `SaleStockDeductionTest` +1: Toggle-Test storniert→versendet bucht nichts (12 grün) |
| 20.07.2026 | **Review-Fix: Mahnwesen-Storno über zentralen Statuswechsel** | Code-Review vor Live-Release: Der Auto-Storno der Zahlungserinnerung (`set_order_status_cancelled`) schrieb `shipped = 10` direkt und rief nur `syncSaleStock()` — **vorbei am in AP-33a extrahierten `OrderStatusService::setShipped()`**. Folge: Bestand kam zurück, aber `margin->cancellation` blieb false, `partner_commission_pending_to`/`shipped_at` blieben gesetzt und die Promotions-/Guthaben-Folgen fehlten — die **Partnerprovision einer versendeten, nie bezahlten Bestellung wäre nach Ablauf der Pending-Frist zahlbar geworden** (Rechnungs-Bestellungen werden vor Zahlungseingang versendet; die Mahn-Abfrage filtert nicht auf Versandstatus). Jetzt ruft der Mahn-Storno `OrderStatusService::setShipped($id, 10)`. | `SaleStockDeductionTest` +1: Ende-zu-Ende über `sendReminder()` — Rückbuchung + Margen-Storno + pending_to/shipped_at genullt (13 grün) |
| 20.07.2026 | **Review-Fix: AP-32-Brücke auf Base64 umgestellt (Connector 1.5.0)** | Code-Review vor Live-Release, drei Befunde in einem Zug behoben: **(1) Die Brücke hätte in Produktion nicht funktioniert** — der Connector 1.4.0 übertrug die rohe `label_url` aus `wp-content/uploads/woocommerce_dhl_label/`, den das DHL-Plugin per „deny from all" schützt → der Laravel-Download lief immer auf 403, das Label wurde nie gespeichert, und der View-Fallback verlinkte dieselbe 403-URL. **(2) SSRF:** der Server rief eine Client-URL ohne Host-/Schema-Prüfung ab. **(3) Timeout-Mismatch:** Download bis 20 s im Request-Pfad vs. 5 s WordPress-Default → Übertragung wäre shopseitig dauerhaft „fehlgeschlagen" (Exists-400 bei jedem Retry). Jetzt: Connector 1.5.0 liest die PDF vom Shop-Dateisystem (`label_path`, realpath-Pinning auf wp-uploads, `%PDF`-Check) und sendet sie **Base64** (`wp_label_pdf`); `storeWpLabel()` dekodiert + validiert (max. 10 MB, `%PDF`), **kein `Http::get` mehr**; `wp_label_url` bleibt reines Herkunfts-Metadatum, View-Fallback-Links entfernt; Connector-Timeout 30 s. Doku `ap-32-brueckenloesung-connector-update.md` + Hinweise überarbeitet (Deployment: Label kommt erst mit Connector 1.5.0 im Shop an). | `WpLabelTransferTest` auf Base64 umgestellt (10 grün; neu: ungültiges Base64/Nicht-PDF verworfen, URL-only = Altconnector blockiert nicht) |
| _offen_ | AP-32 | Versand im System: DHL-Etiketten per API — **neu aufgenommen + Ansatz dokumentiert (§4, 15.07.2026)**; Klärungspunkte (DHL-Konto, Versandprodukte, Druck-Setup) für nächste Anforderungsrunde notiert. **Zwischenstand 17.07.2026: Brückenlösung aktiv** (Label aus dem Shop, s. Protokollzeile) — deckt den Versand-Flow ab, bis die DHL-Klärungen da sind | – |
| _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – |