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:
parent
ddd6dde593
commit
1b9b6adc44
11 changed files with 173 additions and 83 deletions
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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 | – |
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue