diff --git a/app/Http/Controllers/Api/ShoppingUserController.php b/app/Http/Controllers/Api/ShoppingUserController.php index dd5e2f7..b939dd1 100755 --- a/app/Http/Controllers/Api/ShoppingUserController.php +++ b/app/Http/Controllers/Api/ShoppingUserController.php @@ -16,7 +16,6 @@ use App\Services\Shop; use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; @@ -32,6 +31,9 @@ class ShoppingUserController extends Controller protected $member_id = 2; // thomas.krummel@gmail.com + /** AP-32: Obergrenze für per Base64 übertragene DHL-Label-PDFs (dekodiert). */ + private const WP_LABEL_MAX_BYTES = 10 * 1024 * 1024; + /** * @param Request $request * wp_order_numbers[1234, 1234] @@ -368,6 +370,7 @@ class ShoppingUserController extends Controller } $wp_invoice_path = isset($request->wp_invoice_path) ? $request->wp_invoice_path : null; + $wp_label_pdf = isset($request->wp_label_pdf) ? $request->wp_label_pdf : null; $wp_label_url = isset($request->wp_label_url) ? $request->wp_label_url : null; $wp_tracking_number = isset($request->wp_tracking_number) ? $request->wp_tracking_number : null; $wp_advertising = isset($request->wp_advertising) ? $request->wp_advertising : ''; @@ -382,7 +385,7 @@ class ShoppingUserController extends Controller if ($wp_order) { // AP-32 Brückenlösung: DHL-Label aus dem Shop übernehmen (Kopie lokal speichern) - $this->storeWpLabel($shopping_user->shopping_order, $wp_label_url, $wp_tracking_number); + $this->storeWpLabel($shopping_user->shopping_order, $wp_label_pdf, $wp_label_url, $wp_tracking_number); // AP-13: Übertragung inkl. SKU-Zuordnungsergebnis je Position protokollieren ApiOrderLog::recordTransfer($request->wp_order_number, $shopping_user, $shopping_user->shopping_order, $wp_order); @@ -427,6 +430,7 @@ class ShoppingUserController extends Controller { $request->validate([ 'wp_order_number' => 'required|int', + 'wp_label_pdf' => 'nullable|string', 'wp_label_url' => 'nullable|url', 'wp_tracking_number' => 'nullable', ]); @@ -441,7 +445,7 @@ class ShoppingUserController extends Controller } $shopping_order = $shopping_user->shopping_order; - $this->storeWpLabel($shopping_order, $request->wp_label_url, $request->wp_tracking_number); + $this->storeWpLabel($shopping_order, $request->wp_label_pdf, $request->wp_label_url, $request->wp_tracking_number); return response()->json([ 'success' => true, @@ -456,15 +460,18 @@ class ShoppingUserController extends Controller } /** - * Label-URL + Sendungsnummer aus dem WooCommerce-Shop an der Bestellung speichern. - * Das PDF wird als lokale Kopie in den privaten Storage geladen (Auslieferung nur - * über die Admin-Route) — so bleibt es verfügbar, auch wenn es im Shop gelöscht - * wird, und die öffentliche Upload-URL muss nicht verlinkt werden. Ein - * fehlgeschlagener Download darf die Übertragung nie scheitern lassen. + * Label + Sendungsnummer aus dem WooCommerce-Shop an der Bestellung speichern. + * Das PDF kommt seit Connector 1.5.0 als Base64 direkt aus dem Shop-Dateisystem + * mit (Review 20.07.2026: der Label-Ordner im Shop ist per „deny from all" + * geschützt, ein URL-Abruf lief ins Leere — und der serverseitige Abruf einer + * Client-URL war ein SSRF-Risiko und blockierte den Request bis 20 s). Die Kopie + * liegt im privaten Storage (Auslieferung nur über die Admin-Route) und bleibt + * verfügbar, auch wenn das Label im Shop gelöscht wird. Ein ungültiges Label + * darf die Übertragung nie scheitern lassen. */ - private function storeWpLabel($shopping_order, $wp_label_url, $wp_tracking_number): void + private function storeWpLabel($shopping_order, $wp_label_pdf, $wp_label_url, $wp_tracking_number): void { - if (! $shopping_order || (! $wp_label_url && ! $wp_tracking_number)) { + if (! $shopping_order || (! $wp_label_pdf && ! $wp_label_url && ! $wp_tracking_number)) { return; } @@ -475,25 +482,21 @@ class ShoppingUserController extends Controller $shopping_order->tracking = $wp_tracking_number; } + // Reines Anzeige-Metadatum (Herkunft im Shop) — wird nie abgerufen oder verlinkt if ($wp_label_url) { $shopping_order->wp_label_url = $wp_label_url; - try { - $response = Http::timeout(20)->get($wp_label_url); - if ($response->successful() && str_starts_with($response->body(), '%PDF')) { - $path = 'wp-labels/label-'.$shopping_order->id.'.pdf'; - Storage::disk('local')->put($path, $response->body()); - $shopping_order->wp_label_path = $path; - } else { - Log::warning('WP-Label-Download fehlgeschlagen (kein PDF oder HTTP-Fehler)', [ - 'shopping_order_id' => $shopping_order->id, - 'wp_label_url' => $wp_label_url, - 'status' => $response->status(), - ]); - } - } catch (\Throwable $e) { - Log::warning('WP-Label-Download fehlgeschlagen: '.$e->getMessage(), [ + } + + if ($wp_label_pdf) { + $content = base64_decode($wp_label_pdf, true); + if ($content !== false && str_starts_with($content, '%PDF') && strlen($content) <= self::WP_LABEL_MAX_BYTES) { + $path = 'wp-labels/label-'.$shopping_order->id.'.pdf'; + Storage::disk('local')->put($path, $content); + $shopping_order->wp_label_path = $path; + } else { + Log::warning('WP-Label verworfen (kein gültiges Base64-PDF oder zu groß)', [ 'shopping_order_id' => $shopping_order->id, - 'wp_label_url' => $wp_label_url, + 'bytes' => is_string($content) ? strlen($content) : null, ]); } } diff --git a/dev/partnercenter-api/partnercenter-api.php b/dev/partnercenter-api/partnercenter-api.php index eb3de4b..23282de 100644 --- a/dev/partnercenter-api/partnercenter-api.php +++ b/dev/partnercenter-api/partnercenter-api.php @@ -3,7 +3,7 @@ /** * Plugin Name: PARTNERCENTER API Connector * Description: Connect WooCommerce to PARTNERCENTER API - * Version: 1.4.0 + * Version: 1.5.0 * Author: Cross Media Cloud * Author URI: https://www.cross-media-cloud.de * License: GNU General Public License v2 or later diff --git a/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/methods.php b/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/methods.php index b5af391..5f264d1 100644 --- a/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/methods.php +++ b/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/methods.php @@ -45,8 +45,11 @@ trait PARTNERCENTER_API_Methods $url = $this->build_url($endpoint); // Return the POST request responds + // (timeout raised from the 5s WordPress default since 1.5.0 - + // order transfers may carry a base64 encoded DHL label) return wp_safe_remote_post($url, [ 'blocking' => true, + 'timeout' => 30, 'headers' => $this->get_headers(), 'body' => json_encode($body), ] diff --git a/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/order.php b/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/order.php index f998f0d..92aaa56 100644 --- a/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/order.php +++ b/dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/order.php @@ -140,7 +140,10 @@ trait PARTNERCENTER_API_Order /** * Helper to read the DHL label data saved by the "DHL for WooCommerce" plugin. * - * The plugin stores label URL and tracking number(s) as order meta. + * The plugin stores label path/URL and tracking number(s) as order meta. + * Since 1.5.0 the label FILE is sent base64 encoded (wp_label_pdf): the + * label folder inside wp-uploads is protected by "deny from all", so the + * PARTNERCENTER could never fetch the raw label_url server side. * * @return array Empty array when no label exists yet * @@ -153,15 +156,69 @@ trait PARTNERCENTER_API_Order $label_meta = $this->order->get_meta('_pr_shipment_dhl_label_tracking'); // Check if a label was created for this order - if (! is_array($label_meta) || empty($label_meta['label_url'])) { + if (! is_array($label_meta) || (empty($label_meta['label_path']) && empty($label_meta['tracking_number']))) { return []; } - return [ - 'wp_label_url' => $label_meta['label_url'], + $label_data = [ // String or array (multi parcel) - the API accepts both 'wp_tracking_number' => isset($label_meta['tracking_number']) ? $label_meta['tracking_number'] : null, ]; + + // Read the label file from disk and send it along (base64) + $label_pdf = $this->read_label_file(isset($label_meta['label_path']) ? $label_meta['label_path'] : ''); + if ($label_pdf !== false) { + $label_data['wp_label_pdf'] = $label_pdf; + } + + // Reference only - the API stores it as origin metadata, it is never fetched + if (! empty($label_meta['label_url'])) { + $label_data['wp_label_url'] = $label_meta['label_url']; + } + + return $label_data; + } + + /** + * Read a DHL label PDF from disk and return it base64 encoded. + * + * Only PDF files inside the wp-uploads directory are accepted - the + * label_path comes from order meta, never from user input, but we + * validate anyway before touching the filesystem. + * + * @param string $label_path Absolute path from the DHL label meta + * @return string|bool Base64 string or false when unreadable/invalid + * + * @since 1.5.0 + */ + private function read_label_file($label_path) + { + + // No label file saved (tracking-only meta) + if (empty($label_path) || ! is_string($label_path)) { + return false; + } + + // Resolve and pin the file to the uploads directory + $real_path = realpath($label_path); + $uploads = wp_get_upload_dir(); + $uploads_base = realpath($uploads['basedir']); + if ($real_path === false || $uploads_base === false || strpos($real_path, $uploads_base) !== 0) { + return false; + } + + // Only PDF files + if (strtolower(substr($real_path, -4)) !== '.pdf') { + return false; + } + + // Read and verify the content really is a PDF + $contents = file_get_contents($real_path); + if ($contents === false || substr($contents, 0, 4) !== '%PDF') { + return false; + } + + return base64_encode($contents); } /** diff --git a/dev/product management /ap-32-brueckenloesung-connector-update.md b/dev/product management /ap-32-brueckenloesung-connector-update.md index 2248fef..d71be9d 100644 --- a/dev/product management /ap-32-brueckenloesung-connector-update.md +++ b/dev/product management /ap-32-brueckenloesung-connector-update.md @@ -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: diff --git a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md index 15ce35b..93a073d 100644 --- a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md +++ b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md @@ -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 | – | diff --git a/resources/docs/hinweise.md b/resources/docs/hinweise.md index 2b2c369..3b550bd 100644 --- a/resources/docs/hinweise.md +++ b/resources/docs/hinweise.md @@ -127,17 +127,21 @@ im Tabellenkopf = alle sichtbaren) und mit **„Lieferscheine drucken"** bzw. **„+ Rechnungen"** ein einziges PDF für den Stapeldruck erzeugen — fehlende Dokumente werden dabei automatisch miterstellt (ohne E-Mail). -- **DHL-Label aus dem Shop (Brückenlösung, neu 17.07.2026):** Wird das - Versandetikett im WooCommerce-Shop erstellt (Plugin „DHL for WooCommerce") - und die Bestellung danach übertragen, kommen **Label und Sendungsnummer - mit**: Das System lädt eine **eigene Kopie der Label-PDF** herunter (bleibt - verfügbar, auch wenn das Label im Shop gelöscht wird) und zeigt in der - Abwicklung und den Bestell-Details einen **„Label"-Download** plus die - Sendungsnummer. Wird das Label im Shop erst später (neu) erstellt, reicht - der Shop es automatisch nach. Die Sendungsnummer wird über die - Status-Abfrage auch an den Shop zurückgemeldet (Tracking-Link für den - Endkunden). Die vollständige DHL-Anbindung im Partnercenter (Labels direkt - hier erzeugen, AP-32) bleibt geplant. +- **DHL-Label aus dem Shop (Brückenlösung, neu 17.07.2026, überarbeitet + 20.07.2026):** Wird das Versandetikett im WooCommerce-Shop erstellt (Plugin + „DHL for WooCommerce") und die Bestellung danach übertragen, kommen **Label + und Sendungsnummer mit**: Der Shop-Connector (ab **Version 1.5.0**) schickt + die **Label-PDF direkt mit** (der Label-Ordner im Shop ist zugriffsgeschützt, + daher kein Link/Download über die Shop-URL); das System speichert eine + **eigene Kopie** (bleibt verfügbar, auch wenn das Label im Shop gelöscht + wird) und zeigt in der Abwicklung und den Bestell-Details einen + **„Label"-Download** plus die Sendungsnummer. Wird das Label im Shop erst + später (neu) erstellt, reicht der Shop es automatisch nach. Die + Sendungsnummer wird über die Status-Abfrage auch an den Shop zurückgemeldet + (Tracking-Link für den Endkunden). **Wichtig fürs Deployment:** Das Label + kommt erst an, wenn im Shop der Connector 1.5.0 installiert ist. Die + vollständige DHL-Anbindung im Partnercenter (Labels direkt hier erzeugen, + AP-32) bleibt geplant. ### In Vorbereitung / geplant diff --git a/resources/docs/testleitfaden.md b/resources/docs/testleitfaden.md index a301ba3..59ba087 100644 --- a/resources/docs/testleitfaden.md +++ b/resources/docs/testleitfaden.md @@ -165,7 +165,7 @@ - Wird das DHL-Etikett **im WooCommerce-Shop** erstellt und die Bestellung danach übertragen, speichert das System eine **eigene Kopie der Label-PDF** und die **Sendungsnummer** an der Bestellung. - In der Abwicklung erscheint bei der Bestellung ein **„Label"-Download** (neben dem externen Rechnungslink) samt Sendungsnummer; ebenso in den Bestell-Details. - Wird das Label im Shop erst **nach** der Übertragung erstellt (oder neu erstellt), reicht der Shop es automatisch nach — es taucht dann ohne weiteres Zutun in der Abwicklung auf. -- Voraussetzung: das kleine Update des Verbindungs-Plugins im WooCommerce-Shop ist eingespielt. +- Voraussetzung: das Update des Verbindungs-Plugins im WooCommerce-Shop auf **Version 1.5.0** ist eingespielt (überträgt die Label-PDF direkt; mit älterem Connector kommt nur die Sendungsnummer an). **So testen:** Im Shop eine Testbestellung mit DHL-Label versehen → Bestellung ins Partnercenter übertragen → in der **Abwicklung** bei der Bestellung auf **„Label"** klicken (PDF öffnet sich) und die Sendungsnummer prüfen. Danach einmal andersherum: Bestellung zuerst übertragen, Label im Shop erst danach erstellen → Seite neu laden, der Label-Download muss nachträglich erscheinen. diff --git a/resources/views/admin/inventory/orders/index.blade.php b/resources/views/admin/inventory/orders/index.blade.php index 3b9200f..008a4fd 100644 --- a/resources/views/admin/inventory/orders/index.blade.php +++ b/resources/views/admin/inventory/orders/index.blade.php @@ -185,15 +185,12 @@ @else {{ __('extern') }} @endif - {{-- AP-32 Brückenlösung: DHL-Label aus dem Shop (lokale Kopie, sonst Shop-Link) --}} + {{-- AP-32 Brückenlösung: DHL-Label aus dem Shop (lokale Kopie; die Shop-URL + ist zugriffsgeschützt und wird bewusst nicht verlinkt, Review 20.07.2026) --}} @if ($order->wp_label_path) {{ __('Label') }} - @elseif ($order->wp_label_url) - - {{ __('Label') }} - @endif @if ($order->tracking)
{{ $order->tracking }}
diff --git a/resources/views/admin/sales/_detail.blade.php b/resources/views/admin/sales/_detail.blade.php index f7eb083..74179d6 100644 --- a/resources/views/admin/sales/_detail.blade.php +++ b/resources/views/admin/sales/_detail.blade.php @@ -618,20 +618,18 @@ @if ($shopping_order->txaction === 'extern')
- @if ($shopping_order->wp_invoice_path || $shopping_order->wp_label_path || $shopping_order->wp_label_url) + @if ($shopping_order->wp_invoice_path || $shopping_order->wp_label_path || $shopping_order->tracking)
@if ($shopping_order->wp_invoice_path) Link zur Rechnung @endif - {{-- AP-32 Brückenlösung: DHL-Label aus dem WooCommerce-Shop --}} + {{-- AP-32 Brückenlösung: DHL-Label aus dem WooCommerce-Shop (lokale Kopie; + die Shop-URL ist zugriffsgeschützt und wird bewusst nicht verlinkt) --}} @if ($shopping_order->wp_label_path) DHL-Label - @elseif ($shopping_order->wp_label_url) - DHL-Label (Shop) @endif @if ($shopping_order->tracking) Sendungsnummer: {{ $shopping_order->tracking }} diff --git a/tests/Feature/WpLabelTransferTest.php b/tests/Feature/WpLabelTransferTest.php index a384daa..634ca05 100644 --- a/tests/Feature/WpLabelTransferTest.php +++ b/tests/Feature/WpLabelTransferTest.php @@ -9,7 +9,6 @@ use App\Models\ShoppingUser; use App\Models\UserAccount; use App\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use Laravel\Passport\Passport; @@ -95,15 +94,15 @@ function wplabelTransfer(int $wpOrderNumber, array $extra = []) ], $extra)); } -test('uebertragung mit label-url speichert pdf-kopie und sendungsnummer', function () { +test('uebertragung mit base64-label speichert pdf-kopie und sendungsnummer', function () { Mail::fake(); Storage::fake('local'); - Http::fake(['*' => Http::response('%PDF-1.4 label-inhalt', 200)]); Passport::actingAs(wplabelMakeUser()); wplabelMakeProduct(100); wplabelMakeShoppingUser(5711); $response = wplabelTransfer(5711, [ + 'wp_label_pdf' => base64_encode('%PDF-1.4 label-inhalt'), 'wp_label_url' => 'https://shop.example/wp-content/uploads/woocommerce_dhl_label/dhl-label-5711.pdf', 'wp_tracking_number' => '00340434161094000001', ]); @@ -118,12 +117,14 @@ test('uebertragung mit label-url speichert pdf-kopie und sendungsnummer', functi ->and($order->wp_label_url)->toContain('dhl-label-5711.pdf') ->and($order->wp_label_path)->toBe('wp-labels/label-'.$order->id.'.pdf'); Storage::disk('local')->assertExists($order->wp_label_path); + expect(Storage::disk('local')->get($order->wp_label_path))->toBe('%PDF-1.4 label-inhalt'); }); -test('fehlgeschlagener label-download blockiert die uebertragung nicht', function () { +test('nur label-url (alter connector) speichert kein pdf, blockiert aber nicht', function () { + // Abwärtskompatibilität Connector 1.4.0: URL wird als Herkunfts-Metadatum + // gespeichert, aber nie abgerufen — der Shop-Ordner ist zugriffsgeschützt (403) Mail::fake(); Storage::fake('local'); - Http::fake(['*' => Http::response('Serverfehler', 500)]); Passport::actingAs(wplabelMakeUser()); wplabelMakeProduct(100); wplabelMakeShoppingUser(5712); @@ -139,17 +140,21 @@ test('fehlgeschlagener label-download blockiert die uebertragung nicht', functio ->and($order->tracking)->toBe('00340434161094000002'); }); -test('antwort ohne pdf-inhalt wird nicht als label gespeichert', function () { +test('base64 ohne pdf-inhalt oder ungueltiges base64 wird nicht gespeichert', function () { Mail::fake(); Storage::fake('local'); - Http::fake(['*' => Http::response('Login', 200)]); Passport::actingAs(wplabelMakeUser()); wplabelMakeProduct(100); wplabelMakeShoppingUser(5713); - wplabelTransfer(5713, ['wp_label_url' => 'https://shop.example/label.pdf']) + wplabelTransfer(5713, ['wp_label_pdf' => base64_encode('Login')]) ->assertOk()->assertJsonPath('data.wp_label_stored', false); + expect(ShoppingOrder::query()->sole()->wp_label_path)->toBeNull(); + test()->postJson('https://api.'.config('app.api_domain').'/wp/label', [ + 'wp_order_number' => 5713, + 'wp_label_pdf' => 'kein-gueltiges-base64!!!', + ])->assertOk()->assertJsonPath('data.wp_label_stored', false); expect(ShoppingOrder::query()->sole()->wp_label_path)->toBeNull(); }); @@ -168,7 +173,6 @@ test('mehrere sendungsnummern werden zusammengefasst', function () { test('label kann per wp label nachgereicht werden', function () { Mail::fake(); Storage::fake('local'); - Http::fake(['*' => Http::response('%PDF-1.4 nachgereicht', 200)]); Passport::actingAs(wplabelMakeUser()); wplabelMakeProduct(100); wplabelMakeShoppingUser(5715); @@ -179,7 +183,7 @@ test('label kann per wp label nachgereicht werden', function () { test()->postJson('https://api.'.config('app.api_domain').'/wp/label', [ 'wp_order_number' => 5715, - 'wp_label_url' => 'https://shop.example/label-5715.pdf', + 'wp_label_pdf' => base64_encode('%PDF-1.4 nachgereicht'), 'wp_tracking_number' => '00340434161094000005', ])->assertOk()->assertJsonPath('data.wp_label_stored', true);