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

@ -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,
]);
}
}

View file

@ -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

View file

@ -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),
]

View file

@ -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);
}
/**

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 | – |

View file

@ -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

View file

@ -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.

View file

@ -185,15 +185,12 @@
@else
<span class="text-muted small">{{ __('extern') }}</span>
@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)
<a href="{{ route('admin.inventory.orders.document', [$order->id, 'label']) }}" target="_blank" title="{{ __('DHL-Label') }}" class="ml-1">
<span class="fas fa-tag"></span> {{ __('Label') }}
</a>
@elseif ($order->wp_label_url)
<a href="{{ $order->wp_label_url }}" target="_blank" rel="noopener" title="{{ __('DHL-Label (extern im Shop)') }}" class="ml-1">
<span class="fas fa-tag"></span> {{ __('Label') }}
</a>
@endif
@if ($order->tracking)
<div class="small text-muted" title="{{ __('Sendungsnummer') }}">{{ $order->tracking }}</div>

View file

@ -618,20 +618,18 @@
@if ($shopping_order->txaction === 'extern')
<div class="card-body">
@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)
<div class="row">
<div class="col-sm-12">
@if ($shopping_order->wp_invoice_path)
<a href="{{ $shopping_order->wp_invoice_path }}" target="_blank"
class="btn btn-secondary btn-sm">Link zur Rechnung</a>
@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)
<a href="{{ route('admin.inventory.orders.document', [$shopping_order->id, 'label']) }}" target="_blank"
class="btn btn-secondary btn-sm"><span class="fas fa-tag"></span> DHL-Label</a>
@elseif ($shopping_order->wp_label_url)
<a href="{{ $shopping_order->wp_label_url }}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm"><span class="fas fa-tag"></span> DHL-Label (Shop)</a>
@endif
@if ($shopping_order->tracking)
<span class="text-muted small ml-2">Sendungsnummer: {{ $shopping_order->tracking }}</span>

View file

@ -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('<html>Login</html>', 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('<html>Login</html>')])
->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);