Warenwirtschaft: AP-32-Brückenlösung — DHL-Label aus WooCommerce übernehmen

Bestell-Übertragung (wp/order) akzeptiert wp_label_url + wp_tracking_number:
Label-PDF wird als lokale Kopie im privaten Storage gespeichert (Shop-URL ist
öffentlich erratbar; Kopie überlebt Löschung im Shop), Sendungsnummer landet
in shopping_orders.tracking. Neuer Endpoint wp/label zum Nachreichen
(Connector-Hook pr_save_dhl_label_tracking), wp/status liefert die
Sendungsnummer an den Shop zurück. Abwicklungs-Board + Bestell-Details zeigen
Label-Download (copyreader-Dokumentroute type=label, Fallback Shop-Link) und
Sendungsnummer. Download-Fehler blockieren die Übertragung nie.

Doku: Connector-Update-Anleitung (dev/product management), Hinweise,
Testleitfaden §11, Plan-Protokoll. pint.json schließt dev/ (Fremdcode) aus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-17 11:35:40 +00:00
parent a1f94ea427
commit 0eed974576
13 changed files with 611 additions and 25 deletions

View file

@ -166,9 +166,20 @@ class OrderFulfillmentController extends Controller
*/
public function document(int $id, string $type)
{
abort_unless(in_array($type, ['invoice', 'delivery', 'combined'], true), 404);
abort_unless(in_array($type, ['invoice', 'delivery', 'combined', 'label'], true), 404);
$order = ShoppingOrder::query()->findOrFail($id);
// AP-32 Brückenlösung: lokal gespeicherte Kopie des DHL-Labels aus dem
// WooCommerce-Shop (privater Storage — bewusst nicht die öffentliche Shop-URL)
if ($type === 'label') {
abort_unless($order->wp_label_path && Storage::disk('local')->exists($order->wp_label_path), 404);
return Response::make(Storage::disk('local')->get($order->wp_label_path), 200)
->header('Content-Type', 'application/pdf')
->header('Content-disposition', 'inline; filename="DHL-Label-Bestellung-'.$order->id.'.pdf"');
}
abort_unless(Invoice::isInvoice($order), 404);
if ($type === 'combined') {

View file

@ -16,7 +16,10 @@ 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;
use Yard;
class ShoppingUserController extends Controller
@ -64,6 +67,7 @@ class ShoppingUserController extends Controller
'user' => $shopping_user ? true : false,
'order' => ($shopping_user && $shopping_user->shopping_order) ? true : false,
'status' => $shopping_user ? $shopping_user->getAPIShippedType() : false,
'tracking_number' => ($shopping_user && $shopping_user->shopping_order) ? $shopping_user->shopping_order->tracking : null,
];
}
@ -364,6 +368,8 @@ class ShoppingUserController extends Controller
}
$wp_invoice_path = isset($request->wp_invoice_path) ? $request->wp_invoice_path : 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 : '';
$wp_incentives = isset($request->wp_incentives) ? $request->wp_incentives : '';
@ -375,6 +381,9 @@ class ShoppingUserController extends Controller
$wp_order = $this->prepareOrder($wp_order, $shopping_user, $wp_invoice_path, $wp_notice);
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);
// AP-13: Übertragung inkl. SKU-Zuordnungsergebnis je Position protokollieren
ApiOrderLog::recordTransfer($request->wp_order_number, $shopping_user, $shopping_user->shopping_order, $wp_order);
$user = $this->prepareForShow($shopping_user);
@ -385,6 +394,9 @@ class ShoppingUserController extends Controller
'data' => [
'wp_order_number' => $shopping_user->wp_order_number,
'wp_invoice_path' => $wp_invoice_path,
'wp_label_url' => $shopping_user->shopping_order->wp_label_url,
'wp_label_stored' => (bool) $shopping_user->shopping_order->wp_label_path,
'wp_tracking_number' => $shopping_user->shopping_order->tracking,
'wp_advertising' => $wp_advertising,
'wp_incentives' => $wp_incentives,
'wp_order' => $wp_order,
@ -405,6 +417,90 @@ class ShoppingUserController extends Controller
], 500);
}
/**
* AP-32 Brückenlösung: DHL-Label nachreichen bzw. aktualisieren — für den Fall,
* dass das Label im Shop erst NACH der Bestell-Übertragung erstellt (oder neu
* erstellt) wird. Der Connector kann dafür den Plugin-Hook
* `pr_save_dhl_label_tracking` von "DHL for WooCommerce" nutzen.
*/
public function label(Request $request)
{
$request->validate([
'wp_order_number' => 'required|int',
'wp_label_url' => 'nullable|url',
'wp_tracking_number' => 'nullable',
]);
$shopping_user = ShoppingUser::where('wp_order_number', '=', $request->wp_order_number)->first();
if (! $shopping_user || ! $shopping_user->shopping_order) {
return response()->json([
'success' => false,
'message' => 'Order with wp_order_number '.$request->wp_order_number.' not found',
'time' => Carbon::now()->toDateTimeString(),
], 400);
}
$shopping_order = $shopping_user->shopping_order;
$this->storeWpLabel($shopping_order, $request->wp_label_url, $request->wp_tracking_number);
return response()->json([
'success' => true,
'data' => [
'wp_order_number' => $shopping_user->wp_order_number,
'wp_label_url' => $shopping_order->wp_label_url,
'wp_label_stored' => (bool) $shopping_order->wp_label_path,
'wp_tracking_number' => $shopping_order->tracking,
],
'time' => Carbon::now()->toDateTimeString(),
], 200);
}
/**
* 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.
*/
private function storeWpLabel($shopping_order, $wp_label_url, $wp_tracking_number): void
{
if (! $shopping_order || (! $wp_label_url && ! $wp_tracking_number)) {
return;
}
if (is_array($wp_tracking_number)) {
$wp_tracking_number = implode(', ', array_filter($wp_tracking_number));
}
if ($wp_tracking_number) {
$shopping_order->tracking = $wp_tracking_number;
}
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(), [
'shopping_order_id' => $shopping_order->id,
'wp_label_url' => $wp_label_url,
]);
}
}
$shopping_order->save();
}
public function delete(Request $request)
{
$request->validate([