mivita/app/Http/Controllers/DhlShipmentController.php

1137 lines
46 KiB
PHP

<?php
namespace App\Http\Controllers;
use Acme\Dhl\Models\DhlShipment;
use App\Jobs\CancelShipmentJob;
// Old DHL model replaced with new package model
use App\Jobs\CreateReturnLabelJob;
use App\Mail\MailDhlTracking;
use App\Models\Country;
use App\Models\ShoppingOrder;
use App\Services\DhlAddressValidator;
use App\Services\DhlModalService;
use App\Services\DhlProductResolver;
use App\Services\DhlShipmentService;
use App\Services\DhlShipmentWeightCalculator;
use App\Services\DhlTrackingService;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
// Import new DHL package and SettingController
use Yajra\DataTables\Facades\DataTables;
use ZipArchive;
/**
* DHL Shipment Controller
*
* Handles all DHL shipment operations including creation, cancellation,
* tracking, and return labels. Provides both web interface and AJAX endpoints.
*/
class DhlShipmentController extends Controller
{
/**
* Constructor
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('admin')->except(['show', 'track']);
}
/**
* Test the DHL API login credentials and return a JSON response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function testLogin()
{
try {
// Get DHL configuration with admin settings
$settingController = new \App\Http\Controllers\SettingController;
$dhlConfig = $settingController->getDhlConfig();
// Create DhlClient with merged configuration
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
// Test the connection
$connectionTest = $dhlClient->testConnection();
if ($connectionTest) {
$result = [
'success' => true,
'message' => 'DHL API Verbindung erfolgreich getestet! '.config('dhl.config_source').' '.$dhlConfig['base_url'],
'details' => [
'base_url' => $dhlConfig['base_url'],
'using_admin_config' => ! empty($dhlConfig['api_key']),
],
];
} else {
$result = [
'success' => false,
'message' => 'DHL API Verbindung fehlgeschlagen. Prüfen Sie Ihre Zugangsdaten.',
];
}
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Test login failed', [
'error' => $e->getMessage(),
]);
return response()->json([
'success' => false,
'message' => 'DHL API Test fehlgeschlagen: '.$e->getMessage(),
], 500);
}
}
/**
* Display the DHL Cockpit (main overview)
*/
public function index(Request $request): View
{
// Statistics for dashboard widgets
$stats = [
'total_shipments' => DhlShipment::count(),
'pending_shipments' => DhlShipment::where('status', 'pending')->count(),
'shipped_today' => DhlShipment::whereDate('created_at', today())->count(),
'returns_count' => DhlShipment::where('type', 'return')->count(),
];
return view('admin.dhl.cockpit', compact('stats'));
}
/**
* Provides data for the DHL Cockpit DataTable.
*/
public function datatable(Request $request): JsonResponse
{
$query = DhlShipment::with(['shoppingOrder.shopping_user'])
->select('dhl_package_shipments.*') // Explicitly select to avoid conflicts
->orderBy('created_at', 'desc');
// Apply filters from the request
if ($request->filled('type')) {
$query->where('type', $request->get('type'));
}
if ($request->filled('status')) {
$status = DhlShipment::normalizeStatus($request->get('status'));
if ($status === 'canceled') {
$query->whereIn('status', ['canceled', 'cancelled']);
} else {
$query->where('status', $status);
}
}
if ($request->filled('date_from')) {
$query->whereDate('created_at', '>=', $request->get('date_from'));
}
if ($request->filled('date_to')) {
$query->whereDate('created_at', '<=', $request->get('date_to'));
}
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
// Search in shipment fields
$q->where('order_id', 'LIKE', "%{$search}%")
->orWhere('dhl_shipment_no', 'LIKE', "%{$search}%")
->orWhere('routing_code', 'LIKE', "%{$search}%")
->orWhere('related_shipment_id', 'LIKE', "%{$search}%")
->orWhere('billing_number', 'LIKE', "%{$search}%")
->orWhere('firstname', 'LIKE', "%{$search}%")
->orWhere('lastname', 'LIKE', "%{$search}%")
->orWhere('company', 'LIKE', "%{$search}%");
});
}
return DataTables::eloquent($query)
->addColumn('checkbox', function ($shipment) {
return '<label class="custom-control custom-checkbox mb-0"><input type="checkbox" class="custom-control-input shipment-checkbox" value="'.$shipment->id.'"><span class="custom-control-label"></span></label>';
})
->editColumn('id', function ($shipment) {
$class = $shipment->type === 'return' ? 'text-warning font-weight-bold' : 'text-primary font-weight-semibold';
$icon = $shipment->type === 'return' ? '<i class="fas fa-undo mr-1"></i>' : '';
return '<a href="'.route('admin.dhl.show', $shipment).'" class="'.$class.'">'.$icon.'#'.$shipment->id.'</a>';
})
->addColumn('type', function ($shipment) {
if ($shipment->type == 'outbound') {
return '<span class="badge badge-primary"><i class="fas fa-arrow-right"></i> Ausgehend</span>';
} else {
return '<span class="badge badge-warning" style="font-size: 0.9rem; font-weight: 600;"><i class="fas fa-undo"></i> RETOURE</span>';
}
})
->addColumn('order', function ($shipment) {
if ($shipment->order_id) {
return '<a href="'.route('admin_sales_customers_detail', $shipment->order_id).'" class="text-primary">#'.$shipment->order_id.'</a>';
}
return '<span class="text-muted">N/A</span>';
})
->addColumn('customer', function ($shipment) {
return e(trim($shipment->firstname.' '.$shipment->lastname));
})
->editColumn('dhl_shipment_no', function ($shipment) {
return $shipment->dhl_shipment_no ? '<code class="text-success">'.e($shipment->dhl_shipment_no).'</code>' : '<span class="text-muted">-</span>';
})
->addColumn('status', function ($shipment) {
$statusMap = [
'pending' => ['class' => 'warning', 'text' => 'Wartend'],
'created' => ['class' => 'success', 'text' => 'Erstellt'],
'shipped' => ['class' => 'primary', 'text' => 'Versendet'],
'delivered' => ['class' => 'info', 'text' => 'Zugestellt'],
'canceled' => ['class' => 'secondary', 'text' => 'Storniert'],
'failed' => ['class' => 'danger', 'text' => 'Fehler'],
];
$status = DhlShipment::normalizeStatus($shipment->status);
$statusInfo = $statusMap[$status] ?? ['class' => 'light', 'text' => e($shipment->status)];
return '<span class="badge badge-'.$statusInfo['class'].'">'.$statusInfo['text'].'</span>';
})
->addColumn('tracking_status', function ($shipment) {
if ($shipment->tracking_status) {
return '<small class="text-muted">'.e($shipment->tracking_status).'</small>'.
($shipment->last_tracked_at ? '<br><small class="text-muted">'.$shipment->last_tracked_at->format('d.m.Y H:i').'</small>' : '');
}
return '<span class="text-muted">-</span>';
})
->editColumn('weight_kg', function ($shipment) {
return number_format($shipment->weight_kg, 2).' kg';
})
->editColumn('created_at', function ($shipment) {
return $shipment->created_at->format('d.m.Y H:i');
})
->addColumn('actions', function ($shipment) {
$buttons = '<div class="btn-group" role="group">';
$buttons .= '<a href="'.route('admin.dhl.show', $shipment).'" class="btn btn-sm btn-outline-primary" data-toggle="tooltip" title="Details anzeigen"><i class="fas fa-eye"></i></a>';
if ($shipment->label_path) {
$buttons .= '<a href="'.route('admin.dhl.download-label', $shipment).'" class="btn btn-sm btn-outline-success" data-toggle="tooltip" title="Label herunterladen"><i class="fas fa-download"></i></a>';
}
// Email button
if ($shipment->dhl_shipment_no && $shipment->canSendTrackingEmail()) {
$emailTitle = $shipment->wasTrackingEmailSent()
? 'Tracking-E-Mail erneut senden (gesendet: '.$shipment->tracking_email_sent_at->format('d.m.Y H:i').')'
: 'Tracking-E-Mail senden';
$emailClass = $shipment->wasTrackingEmailSent() ? 'btn-success' : 'btn-outline-info';
$buttons .= '<button type="button" class="btn btn-sm '.$emailClass.' send-tracking-email-btn" data-shipment-id="'.$shipment->id.'" data-toggle="tooltip" title="'.$emailTitle.'"><i class="fas fa-envelope"></i></button>';
}
// Cancel button
if ($shipment->canCancel()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-danger cancel-shipment-btn" data-shipment-id="'.$shipment->id.'" data-toggle="tooltip" title="Sendung stornieren"><i class="fas fa-ban"></i></button>';
}
// Return label button (for outbound shipments without existing return)
if ($shipment->type == 'outbound' && ! $shipment->returns()->count()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-info create-return-btn" data-shipment-id="'.$shipment->id.'" data-toggle="tooltip" title="Retourenlabel erstellen"><i class="fas fa-undo"></i></button>';
}
$buttons .= '</div>';
return $buttons;
})
->addColumn('DT_RowClass', function ($shipment) {
return $shipment->type === 'return' ? 'return-shipment' : '';
})
->rawColumns(['checkbox', 'id', 'type', 'order', 'customer', 'dhl_shipment_no', 'status', 'tracking_status', 'actions'])
->make(true);
}
/**
* Show the form for creating a new shipment
*
* @return View
*/
public function create(ShoppingOrder $order): View|\Illuminate\Http\RedirectResponse
{
// Check if order already has a shipment
$existingShipment = DhlShipment::where('shopping_order_id', $order->id)
->where('type', 'outbound')
->first();
if ($existingShipment) {
return redirect()->route('admin.dhl.show', $existingShipment)
->with('warning', 'Für diese Bestellung existiert bereits eine Sendung.');
}
return view('admin.dhl.create', compact('order'));
}
/**
* Store a new shipment (async via queue)
*/
public function store(Request $request): JsonResponse
{
try {
// Use DhlModalService for validation
$dhlModalService = new DhlModalService;
$validationResult = $dhlModalService->validateShipmentData($request->all());
if (! $validationResult['valid']) {
return response()->json([
'success' => false,
'message' => 'Validierungsfehler: '.implode(', ', $validationResult['errors']),
], 422);
}
// Basic Laravel validation as fallback
$request->validate([
'order_id' => 'required|exists:shopping_orders,id',
'weight' => 'required|numeric|min:0.1|max:31.5',
'product_code' => 'sometimes|string',
'reference' => 'nullable|string|max:35',
'priority' => 'sometimes|string|in:normal,high',
'auto_track' => 'sometimes|boolean',
// Shipping address validation
'shipping_firstname' => 'required|string|max:50',
'shipping_lastname' => 'required|string|max:50',
'shipping_company' => 'nullable|string|max:100',
'shipping_address' => 'required|string|max:100',
'shipping_houseNumber' => 'required|string|max:50',
'shipping_zipcode' => 'required|string|max:10',
'shipping_city' => 'required|string|max:50',
'shipping_country_id' => 'required|exists:countries,id',
'shipping_phone' => 'nullable|string|max:20',
'shipping_email' => 'required|email|max:100',
'shipping_postnumber' => 'nullable|string|max:20',
]);
$order = ShoppingOrder::findOrFail($request->order_id);
$shipmentWeight = max(
(float) $request->weight,
(new DhlShipmentWeightCalculator)->calculate($order)
);
// Check if shipment already exists
/* $existingShipment = DhlShipment::where('shopping_order_id', $order->id)
->where('type', 'outbound')
->first();
if ($existingShipment) {
return response()->json([
'success' => false,
'message' => 'Für diese Bestellung existiert bereits eine Sendung.'
], 422);
}
*/
// Use service to prepare address data
$shippingAddress = $dhlModalService->prepareAddressForApi($request->all());
// Prepare options for shipment creation
$options = [
'product_code' => $request->get('product_code', 'V01PAK'),
'reference' => $request->get('reference'),
'priority' => $request->get('priority', 'normal'),
'auto_track' => $request->get('auto_track', true),
'shipping_address' => $shippingAddress,
'services' => $request->get('services', []),
'dimensions' => $request->only(['length', 'width', 'height']),
];
// Use DhlShipmentService (handles queue/sync automatically based on config)
$dhlShipmentService = new DhlShipmentService;
$result = $dhlShipmentService->createShipment($order, $shipmentWeight, $options);
Log::info('[DHL Controller] Shipment creation processed', [
'order_id' => $order->id,
'weight' => $shipmentWeight,
'requested_weight' => $request->weight,
'queued' => $result['queued'] ?? false,
'success' => $result['success'] ?? false,
]);
if (! ($result['success'] ?? false)) {
return response()->json($result, ($result['type'] ?? null) === 'dhl_address_validation' ? 422 : 500);
}
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch shipment creation', [
'error' => $e->getMessage(),
'order_id' => $request->order_id ?? 'unknown',
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen der Sendung: '.$e->getMessage(),
], 500);
}
}
public function validateAddress(Request $request, DhlAddressValidator $validator): JsonResponse
{
$country = $request->filled('shipping_country_id')
? Country::find($request->get('shipping_country_id'))
: null;
$resolver = new DhlProductResolver;
$result = $validator->validate(array_merge($request->all(), [
'shipping_country_code' => $country?->code,
]));
$errors = $result['errors'];
$warnings = $result['warnings'];
$product = [
'code' => $request->get('product_code'),
'scope' => null,
'scope_label' => 'Nicht geprüft',
'country_code' => $country?->code,
'country_label' => $country?->getLocated(),
];
if ($country) {
try {
$resolvedProductCode = $resolver->resolveProductCode(
$country->code,
$request->get('product_code'),
config('dhl.default_product', 'V01PAK')
);
$product = [
'code' => $resolvedProductCode,
'scope' => $resolver->getProductScope($resolvedProductCode),
'scope_label' => $resolver->getProductScopeLabel($resolvedProductCode),
'country_code' => $country->code,
'country_label' => $country->getLocated(),
];
} catch (\InvalidArgumentException $e) {
$errors[] = $e->getMessage();
}
}
$status = 'valid';
if ($errors !== []) {
$status = 'error';
} elseif ($warnings !== []) {
$status = 'warning';
}
return response()->json([
'success' => $errors === [],
'status' => $status,
'can_create_label' => $errors === [],
'errors' => array_values(array_unique($errors)),
'warnings' => array_values(array_unique($warnings)),
'message' => $this->addressValidationMessage($status),
'preflight' => [
'product' => $product,
'address' => [
'status' => $result['status'],
'normalized' => $result['normalized'],
'validation_available' => $result['validation_available'],
'validation_level' => $result['validation_level'],
'validation_message' => $result['validation_message'],
],
],
], $errors === [] ? 200 : 422);
}
private function addressValidationMessage(string $status): string
{
return match ($status) {
'valid' => 'Adresse ist formal versandfähig.',
'warning' => 'Adresse ist formal versandfähig, sollte aber vor der Labelerstellung geprüft werden.',
default => 'Adresse ist nicht versandfähig. Bitte korrigieren Sie die markierten Felder.',
};
}
/**
* Display the specified shipment
*/
public function show(DhlShipment $shipment): View
{
$shipment->load([
'shoppingOrder.shopping_user',
'relatedShipment',
'trackingEvents' => fn ($q) => $q->orderBy('event_time', 'desc'),
]);
return view('admin.dhl.show', compact('shipment'));
}
/**
* Cancel the specified shipment
*/
public function cancel(Request $request, DhlShipment $shipment): JsonResponse
{
try {
// Validate cancellation is possible
if (! $shipment->canCancel()) {
return response()->json([
'success' => false,
'message' => 'Diese Sendung kann nicht mehr storniert werden.',
], 422);
}
// Use DhlShipmentService (handles queue/sync automatically based on config)
$options = [
'priority' => $request->get('priority', 'normal'),
];
$dhlShipmentService = new DhlShipmentService;
$result = $dhlShipmentService->cancelShipment($shipment, $options);
Log::info('[DHL Controller] Shipment cancellation processed', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'queued' => $result['queued'] ?? false,
'success' => $result['success'] ?? false,
]);
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to process shipment cancellation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Stornieren der Sendung: '.$e->getMessage(),
], 500);
}
}
/**
* Create return label for the specified shipment
*/
public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse
{
try {
// Validate return label creation is possible
if ($shipment->type !== 'outbound') {
return response()->json([
'success' => false,
'message' => 'Retourenlabels können nur für ausgehende Sendungen erstellt werden.',
], 422);
}
// Check if return label already exists
$existingReturn = DhlShipment::where('related_shipment_id', $shipment->id)
->where('type', 'return')
->first();
if ($existingReturn) {
return response()->json([
'success' => false,
'message' => 'Für diese Sendung existiert bereits ein Retourenlabel.',
], 422);
}
// Check DHL_USE_QUEUE configuration
$settingController = new SettingController;
$dhlConfig = $settingController->getDhlConfig();
$useQueue = $dhlConfig['use_queue'] ?? false;
if ($useQueue) {
// Dispatch return label creation job
$options = [
'auto_track' => $request->get('auto_track', false),
'priority' => $request->get('priority', 'normal'),
];
CreateReturnLabelJob::dispatch($shipment, $options);
Log::info('[DHL Controller] Return label creation job dispatched', [
'original_shipment_id' => $shipment->id,
'shipment_number' => $shipment->dhl_shipment_no,
]);
return response()->json([
'success' => true,
'message' => 'Retourenlabel wird im Hintergrund erstellt. Dies kann einige Sekunden dauern.',
]);
} else {
// Create synchronously
$result = $this->createReturnLabelSync($shipment);
return response()->json($result);
}
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to create return label', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen des Retourenlabels: '.$e->getMessage(),
], 500);
}
}
/**
* Get billing address for return label (used when original delivery was to Packstation)
*/
private function getBillingAddressForReturn($shippingUser, array $recipient): array
{
if (! $shippingUser) {
Log::warning('[DHL Controller] No shipping user found, using recipient country only', [
'order_recipient_country' => $recipient['country'] ?? null,
]);
// Fallback: use recipient data but without Packstation fields.
// We keep ISO-2 country codes here so that ReturnsService /
// DhlProductResolver can normalize / validate them consistently.
return [
'name' => trim(($recipient['firstname'] ?? '').' '.($recipient['lastname'] ?? '')),
'name2' => $recipient['company'] ?? '',
'street' => 'Adresse fehlt',
'houseNumber' => '',
'postalCode' => $recipient['postalCode'] ?? '',
'city' => $recipient['city'] ?? '',
'country' => $recipient['country'] ?? DhlProductResolver::DOMESTIC_COUNTRY,
'email' => $recipient['email'] ?? '',
'phone' => $recipient['phone'] ?? '',
];
}
// Parse billing address to extract street and house number
$billingAddress = trim($shippingUser->billing_address ?? '');
$street = $billingAddress;
$houseNumber = '';
// Try to extract house number from address
if (preg_match('/^(.+?)\s+(\d+[a-zA-Z]?[-\/\d]*)$/u', $billingAddress, $matches)) {
$street = trim($matches[1]);
$houseNumber = trim($matches[2]);
}
return [
'name' => trim(($shippingUser->billing_firstname ?? '').' '.($shippingUser->billing_lastname ?? '')),
'name2' => $shippingUser->billing_company ?? '',
'street' => $street,
'houseNumber' => $houseNumber,
'postalCode' => $shippingUser->billing_zipcode ?? '',
'city' => $shippingUser->billing_city ?? '',
'country' => $shippingUser->billing_country?->code ?? DhlProductResolver::DOMESTIC_COUNTRY,
'email' => $shippingUser->billing_email ?? '',
'phone' => $shippingUser->billing_phone ?? '',
];
}
/**
* Create return label synchronously
*/
private function createReturnLabelSync(DhlShipment $shipment): array
{
try {
Log::info('[DHL Controller] Creating return label synchronously', [
'original_shipment_id' => $shipment->id,
]);
// Get DHL configuration
$settingController = new SettingController;
$dhlConfig = $settingController->getDhlConfig();
// Initialize DHL client
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
// Use ReturnsService instead of ShippingService
$returnsService = new \Acme\Dhl\Services\ReturnsService($dhlClient);
// Prepare return label data
$order = $shipment->shoppingOrder;
$recipient = $shipment->recipient ?? [];
// Check if this is a Packstation delivery - use billing address as return sender
$hasPostNumber = ! empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
if ($hasPostNumber) {
Log::info('[DHL Controller] Packstation detected - using billing address for return sender', [
'shipment_id' => $shipment->id,
'order_id' => $order->id,
]);
// Load billing address from order
$shippingUser = $order->shopping_user;
$shipperAddress = $this->getBillingAddressForReturn($shippingUser, $recipient);
} else {
// Use original recipient address (normal delivery)
$shipperAddress = [
'name' => trim(($recipient['firstname'] ?? '').' '.($recipient['lastname'] ?? '')),
'name2' => $recipient['company'] ?? '',
'street' => $recipient['street'] ?? '',
'houseNumber' => $recipient['houseNumber'] ?? '',
'postalCode' => $recipient['postalCode'] ?? '',
'city' => $recipient['city'] ?? '',
'country' => $recipient['country'] ?? DhlProductResolver::DOMESTIC_COUNTRY,
'email' => $recipient['email'] ?? '',
'phone' => $recipient['phone'] ?? '',
];
}
$returnData = [
'order_id' => $order->id,
'original_shipment_id' => $shipment->id,
'weight_kg' => $shipment->weight_kg,
'label_format' => $shipment->label_format ?? 'PDF',
// Shipper: Customer sends back to us (using billing address for Packstation)
'shipper' => $shipperAddress,
// Consignee: Our warehouse
'consignee' => [
'name' => $dhlConfig['sender']['company'] ?? 'mivita care gmbh',
'name2' => $dhlConfig['sender']['name'] ?? '',
'street' => $dhlConfig['sender']['street'] ?? 'Leinfeld',
'houseNumber' => $dhlConfig['sender']['house_number'] ?? '2',
'postalCode' => $dhlConfig['sender']['postalCode'] ?? '87755',
'city' => $dhlConfig['sender']['city'] ?? 'Kirchhaslach',
'country' => $dhlConfig['sender']['country'] ?? DhlProductResolver::DOMESTIC_COUNTRY,
'email' => $dhlConfig['sender']['email'] ?? 'versand@mivita.care',
'phone' => $dhlConfig['sender']['phone'] ?? '+49 123 456789',
],
];
// Create the return label using ReturnsService
$result = $returnsService->createReturn($returnData);
Log::info('[DHL Controller] Return label created successfully (sync)', [
'original_shipment_id' => $shipment->id,
'return_shipment_number' => $result['returnNumber'] ?? 'N/A',
]);
return [
'success' => true,
'message' => 'Retourenlabel wurde erfolgreich erstellt!',
'shipment_number' => $result['returnNumber'] ?? null,
'return_shipment' => $result['returnShipment'] ?? null,
];
} catch (Exception $e) {
Log::error('[DHL Controller] Return label creation failed (sync)', [
'original_shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'Fehler beim Erstellen des Retourenlabels: '.$e->getMessage(),
];
}
}
/**
* Update tracking status for the specified shipment
*/
public function updateTracking(DhlShipment $shipment): JsonResponse
{
try {
if (! $shipment->dhl_shipment_no) {
return response()->json([
'success' => false,
'message' => 'Keine DHL-Sendungsnummer verfügbar.',
], 422);
}
// Use DhlTrackingService (handles queue/sync automatically based on config)
$dhlTrackingService = new DhlTrackingService;
$result = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]);
Log::info('[DHL Controller] Tracking update processed', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'queued' => $result['queued'] ?? false,
'success' => $result['success'] ?? false,
]);
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to process tracking update', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: '.$e->getMessage(),
], 500);
}
}
/**
* Send tracking email to customer (supports multiple shipments per order)
*/
public function sendTrackingEmail(DhlShipment $shipment): JsonResponse
{
try {
// Check if shipment has tracking number
if (! $shipment->dhl_shipment_no) {
return response()->json([
'success' => false,
'message' => 'Keine DHL-Sendungsnummer verfügbar.',
], 422);
}
// Check if shipment can send email
if (! $shipment->canSendTrackingEmail()) {
return response()->json([
'success' => false,
'message' => 'E-Mail kann nicht gesendet werden. Bestellung oder E-Mail-Adresse fehlt.',
], 422);
}
$order = $shipment->shoppingOrder;
// Determine recipient email: prefer shipment email, fallback to shopping user email
$recipientEmail = null;
if (! empty($shipment->email)) {
$recipientEmail = $shipment->email;
} elseif ($order->shopping_user && ! empty($order->shopping_user->email)) {
$recipientEmail = $order->shopping_user->email;
}
if (! $recipientEmail) {
return response()->json([
'success' => false,
'message' => 'Keine Empfänger-E-Mail-Adresse verfügbar.',
], 422);
}
// Collect all shipments for this order that have tracking numbers
$allShipments = DhlShipment::where('order_id', $order->id)
->whereNotNull('dhl_shipment_no')
->whereIn('status', ['created', 'in_transit', 'out_for_delivery'])
->orderBy('created_at', 'asc')
->get();
// If no shipments found, use only the current one
if ($allShipments->isEmpty()) {
$allShipments = collect([$shipment]);
}
// Send email with all shipments
Mail::to($recipientEmail)->send(new MailDhlTracking($allShipments, $order));
// Mark all included shipments as sent
foreach ($allShipments as $s) {
$s->markTrackingEmailSent('manual', $recipientEmail, $allShipments);
}
Log::info('[DHL Controller] Tracking email sent', [
'shipment_ids' => $allShipments->pluck('id')->toArray(),
'shipments_count' => $allShipments->count(),
'dhl_shipment_nos' => $allShipments->pluck('dhl_shipment_no')->toArray(),
'email' => $recipientEmail,
'type' => 'manual',
]);
$message = $allShipments->count() > 1
? "Tracking-E-Mail mit {$allShipments->count()} Sendungen wurde erfolgreich an {$recipientEmail} gesendet."
: "Tracking-E-Mail wurde erfolgreich an {$recipientEmail} gesendet.";
return response()->json([
'success' => true,
'message' => $message,
'sent_at' => now()->format('d.m.Y H:i'),
'shipments_count' => $allShipments->count(),
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to send tracking email', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Senden der Tracking-E-Mail: '.$e->getMessage(),
], 500);
}
}
/**
* Download shipping label
*/
public function downloadLabel(DhlShipment $shipment): Response
{
try {
if (! $shipment->label_path || ! Storage::exists($shipment->label_path)) {
abort(404, 'Versandlabel nicht gefunden.');
}
$labelContent = Storage::get($shipment->label_path);
// Generate descriptive filename
$filename = $this->generateLabelFilename($shipment);
return response($labelContent, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', "attachment; filename=\"{$filename}\"");
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to download label', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
'label_path' => $shipment->label_path,
]);
abort(500, 'Fehler beim Download des Versandlabels.');
}
}
/**
* Generate descriptive filename for DHL label
* Format: DHL-Kundenname-Sendungsnummer-Datum.pdf
* Example: DHL-Geraldine-Seebacher-0034043333301020015589177-15092025.pdf
*/
private function generateLabelFilename(DhlShipment $shipment): string
{
// Load order with customer data
$customerName = $shipment->firstname.'_'.$shipment->lastname;
if ($shipment->company) {
$customerName = $shipment->company;
}
// Clean customer name for filename (remove special characters)
$customerName = preg_replace('/[^a-zA-Z0-9\-]/', '', $customerName);
$customerName = preg_replace('/-+/', '-', $customerName); // Remove multiple dashes
$customerName = trim($customerName, '-'); // Remove leading/trailing dashes
// Get shipment number
$shipmentNumber = $shipment->dhl_shipment_no ?: $shipment->id;
// Get creation date
$date = $shipment->created_at->format('d_m_Y');
// Build filename
$filename = sprintf(
'DHL-%s-%s-%s.pdf',
$customerName,
$shipmentNumber,
$date
);
// Ensure filename is not too long (max 255 characters)
if (strlen($filename) > 255) {
$maxCustomerLength = 255 - strlen('DHL--'.$shipmentNumber.'-'.$date.'.pdf');
$customerName = substr($customerName, 0, max(10, $maxCustomerLength));
$filename = sprintf(
'DHL-%s-%s-%s.pdf',
$customerName,
$shipmentNumber,
$date
);
}
return $filename;
}
/**
* Batch operations (multiple shipments)
*
* @return JsonResponse|BinaryFileResponse
*/
public function batchAction(Request $request)
{
try {
$request->validate([
'action' => 'required|in:cancel,download_labels,update_tracking',
'shipment_ids' => 'required|array|min:1',
'shipment_ids.*' => 'exists:dhl_package_shipments,id',
]);
$shipmentIds = $request->shipment_ids;
$action = $request->action;
$processed = 0;
$errors = [];
$labels = []; // For batch label download
foreach ($shipmentIds as $shipmentId) {
try {
$shipment = DhlShipment::findOrFail($shipmentId);
switch ($action) {
case 'cancel':
if ($shipment->canCancel()) {
CancelShipmentJob::dispatch($shipment);
$processed++;
} else {
$errors[] = "Sendung {$shipment->shipment_number} kann nicht storniert werden.";
}
break;
case 'update_tracking':
if ($shipment->dhl_shipment_no) {
$dhlTrackingService = new DhlTrackingService;
$trackingResult = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]);
if ($trackingResult['success']) {
$processed++;
} else {
$errors[] = "Sendung #{$shipment->id}: ".$trackingResult['message'];
}
} else {
$errors[] = "Sendung #{$shipment->id} hat keine DHL-Sendungsnummer.";
}
break;
case 'download_labels':
if ($shipment->label_path && Storage::exists($shipment->label_path)) {
$labels[] = [
'shipment' => $shipment,
'filename' => $this->generateLabelFilename($shipment),
'path' => $shipment->label_path,
];
$processed++;
} else {
$errors[] = "Sendung #{$shipment->id} hat kein verfügbares Label.";
}
break;
}
} catch (Exception $e) {
$errors[] = "Fehler bei Sendung {$shipmentId}: ".$e->getMessage();
}
}
// Handle batch label download
if ($action === 'download_labels' && ! empty($labels)) {
return $this->createLabelsZip($labels);
}
Log::info('[DHL Controller] Batch action executed', [
'action' => $action,
'processed' => $processed,
'errors_count' => count($errors),
]);
return response()->json([
'success' => $processed > 0,
'message' => sprintf('%d Sendungen verarbeitet.', $processed),
'processed' => $processed,
'errors' => $errors,
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Batch action failed', [
'error' => $e->getMessage(),
'action' => $request->action ?? 'unknown',
]);
return response()->json([
'success' => false,
'message' => 'Fehler bei der Stapelverarbeitung: '.$e->getMessage(),
], 500);
}
}
/**
* Public tracking page (for customers)
*/
public function track(Request $request): View|JsonResponse
{
if ($request->expectsJson()) {
$request->validate([
'tracking_number' => 'required|string|min:10',
]);
try {
$shipment = DhlShipment::where('dhl_shipment_no', $request->tracking_number)->first();
if (! $shipment) {
return response()->json([
'success' => false,
'message' => 'Sendung nicht gefunden.',
], 404);
}
// Use DhlTrackingService for tracking update
$dhlTrackingService = new DhlTrackingService;
$trackingResult = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]);
return response()->json([
'success' => $trackingResult['success'],
'message' => $trackingResult['message'],
'data' => [
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'tracking_status' => $shipment->tracking_status,
'last_tracked_at' => $shipment->last_tracked_at?->format('d.m.Y H:i'),
],
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Public tracking failed', [
'error' => $e->getMessage(),
'tracking_number' => $request->tracking_number ?? 'unknown',
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Abrufen der Tracking-Informationen.',
], 500);
}
}
return view('public.tracking');
}
/**
* Create ZIP file with multiple labels
*
* @param array $labels Array of label data
* @return Response|BinaryFileResponse
*/
private function createLabelsZip(array $labels)
{
try {
$zip = new ZipArchive;
$zipFilename = 'dhl_labels_'.date('Y-m-d_H-i-s').'.zip';
$zipPath = storage_path('app/temp/'.$zipFilename);
// Ensure temp directory exists
if (! file_exists(storage_path('app/temp'))) {
mkdir(storage_path('app/temp'), 0755, true);
}
if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
throw new Exception('ZIP-Datei konnte nicht erstellt werden.');
}
$addedFiles = 0;
foreach ($labels as $labelData) {
$shipment = $labelData['shipment'];
$filename = $labelData['filename'];
$filePath = $labelData['path'];
if (Storage::exists($filePath)) {
$content = Storage::get($filePath);
$zip->addFromString($filename, $content);
$addedFiles++;
}
}
$zip->close();
if ($addedFiles === 0) {
throw new Exception('Keine Labels konnten zur ZIP-Datei hinzugefügt werden.');
}
Log::info('[DHL Controller] Labels ZIP created', [
'zip_file' => $zipFilename,
'files_count' => $addedFiles,
'total_labels' => count($labels),
]);
return response()->download($zipPath, $zipFilename)->deleteFileAfterSend(true);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to create labels ZIP', [
'error' => $e->getMessage(),
'labels_count' => count($labels),
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen der ZIP-Datei: '.$e->getMessage(),
], 500);
}
}
}