27-05-2026 Update DHL Modul v2.0
This commit is contained in:
parent
53bdba33cd
commit
036595be94
41 changed files with 3346 additions and 310 deletions
|
|
@ -244,14 +244,7 @@ class DhlUpdateTracking extends Command
|
|||
*/
|
||||
private function shouldSendEmail(DhlShipment $shipment, string $oldStatus): bool
|
||||
{
|
||||
// E-Mail nur senden wenn:
|
||||
// 1. Status ist jetzt "in_transit"
|
||||
// 2. Vorheriger Status war NICHT "in_transit" (also Status hat sich geändert)
|
||||
// 3. Noch keine E-Mail gesendet wurde
|
||||
return $shipment->status === 'in_transit'
|
||||
&& $oldStatus !== 'in_transit'
|
||||
&& ! $shipment->wasTrackingEmailSent()
|
||||
&& $shipment->canSendTrackingEmail();
|
||||
return $shipment->shouldTriggerTrackingEmail($oldStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -282,7 +275,7 @@ class DhlUpdateTracking extends Command
|
|||
|
||||
// Sammle alle Sendungen für diese Bestellung, die noch keine E-Mail erhalten haben
|
||||
$allShipments = DhlShipment::where('order_id', $order->id)
|
||||
->where('status', 'in_transit')
|
||||
->whereIn('status', DhlShipment::TRACKING_EMAIL_TRIGGER_STATUSES)
|
||||
->whereNotNull('dhl_shipment_no')
|
||||
->whereNull('tracking_email_sent_at')
|
||||
->get();
|
||||
|
|
@ -297,7 +290,7 @@ class DhlUpdateTracking extends Command
|
|||
|
||||
// Markiere alle Sendungen als versendet
|
||||
foreach ($allShipments as $s) {
|
||||
$s->markTrackingEmailSent('auto');
|
||||
$s->markTrackingEmailSent('auto', $recipientEmail, $allShipments);
|
||||
}
|
||||
|
||||
Log::info('[DHL Cron] Tracking email sent automatically', [
|
||||
|
|
|
|||
|
|
@ -7,9 +7,13 @@ 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;
|
||||
|
|
@ -122,7 +126,12 @@ class DhlShipmentController extends Controller
|
|||
$query->where('type', $request->get('type'));
|
||||
}
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->get('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'));
|
||||
|
|
@ -181,10 +190,11 @@ class DhlShipmentController extends Controller
|
|||
'created' => ['class' => 'success', 'text' => 'Erstellt'],
|
||||
'shipped' => ['class' => 'primary', 'text' => 'Versendet'],
|
||||
'delivered' => ['class' => 'info', 'text' => 'Zugestellt'],
|
||||
'cancelled' => ['class' => 'secondary', 'text' => 'Storniert'],
|
||||
'canceled' => ['class' => 'secondary', 'text' => 'Storniert'],
|
||||
'failed' => ['class' => 'danger', 'text' => 'Fehler'],
|
||||
];
|
||||
$statusInfo = $statusMap[$shipment->status] ?? ['class' => 'light', 'text' => e($shipment->status)];
|
||||
$status = DhlShipment::normalizeStatus($shipment->status);
|
||||
$statusInfo = $statusMap[$status] ?? ['class' => 'light', 'text' => e($shipment->status)];
|
||||
|
||||
return '<span class="badge badge-'.$statusInfo['class'].'">'.$statusInfo['text'].'</span>';
|
||||
})
|
||||
|
|
@ -277,6 +287,7 @@ class DhlShipmentController extends Controller
|
|||
'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
|
||||
|
|
@ -294,6 +305,10 @@ class DhlShipmentController extends Controller
|
|||
]);
|
||||
|
||||
$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)
|
||||
|
|
@ -314,6 +329,7 @@ class DhlShipmentController extends Controller
|
|||
// 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,
|
||||
|
|
@ -323,15 +339,20 @@ class DhlShipmentController extends Controller
|
|||
|
||||
// Use DhlShipmentService (handles queue/sync automatically based on config)
|
||||
$dhlShipmentService = new DhlShipmentService;
|
||||
$result = $dhlShipmentService->createShipment($order, (float) $request->weight, $options);
|
||||
$result = $dhlShipmentService->createShipment($order, $shipmentWeight, $options);
|
||||
|
||||
Log::info('[DHL Controller] Shipment creation processed', [
|
||||
'order_id' => $order->id,
|
||||
'weight' => $request->weight,
|
||||
'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', [
|
||||
|
|
@ -346,6 +367,81 @@ class DhlShipmentController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
@ -719,7 +815,7 @@ class DhlShipmentController extends Controller
|
|||
|
||||
// Mark all included shipments as sent
|
||||
foreach ($allShipments as $s) {
|
||||
$s->markTrackingEmailSent('manual');
|
||||
$s->markTrackingEmailSent('manual', $recipientEmail, $allShipments);
|
||||
}
|
||||
|
||||
Log::info('[DHL Controller] Tracking email sent', [
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@ class ModalController extends Controller
|
|||
'V01PAK' => 'DHL Paket (National)',
|
||||
'V53WPAK' => 'DHL Paket International',
|
||||
],
|
||||
'productSuggestions' => (new \App\Services\DhlProductResolver)->getProductSuggestionsByCountry(),
|
||||
'selectedProductCode' => 'V01PAK',
|
||||
'errors' => ['Fehler beim Laden der Daten: '.$e->getMessage()],
|
||||
'warnings' => [],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Services\DhlProductResolver;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Request;
|
||||
|
||||
class SettingController extends Controller
|
||||
|
|
@ -36,9 +38,9 @@ class SettingController extends Controller
|
|||
// DHL-spezifische Behandlung
|
||||
if ($data['action'] === 'save_dhl') {
|
||||
$this->updateDhlConfigCache();
|
||||
\Session()->flash('alert-save-dhl', 'DHL Konfiguration erfolgreich gespeichert!');
|
||||
Session::flash('alert-save-dhl', 'DHL Konfiguration erfolgreich gespeichert!');
|
||||
} else {
|
||||
\Session()->flash('alert-save', '1');
|
||||
Session::flash('alert-save', '1');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,10 +74,12 @@ class SettingController extends Controller
|
|||
'test_mode' => config('dhl.legacy.test_mode', true),
|
||||
|
||||
// Product Settings
|
||||
'default_product' => $this->getConfigValue('dhl_product', config('dhl.default_product'), $useEnvPriority),
|
||||
'default_product' => $this->normalizeDhlProductCode($this->getConfigValue('dhl_product', config('dhl.default_product'), $useEnvPriority)),
|
||||
'international_countries' => $this->getDhlInternationalCountries($useEnvPriority),
|
||||
'label_format' => $this->getConfigValue('dhl_label_format', config('dhl.label_format'), $useEnvPriority),
|
||||
'print_format' => $this->getConfigValue('dhl_print_format', config('dhl.print_format'), $useEnvPriority),
|
||||
'retoure_print_format' => $this->getConfigValue('dhl_retoure_print_format', config('dhl.retoure_print_format'), $useEnvPriority),
|
||||
'print_only_if_codeable' => (bool) $this->getConfigValue('dhl_print_only_if_codeable', config('dhl.print_only_if_codeable'), $useEnvPriority),
|
||||
'use_queue' => $this->getConfigValue('dhl_use_queue', config('dhl.use_queue'), $useEnvPriority),
|
||||
|
||||
// Sender Address
|
||||
|
|
@ -94,7 +98,7 @@ class SettingController extends Controller
|
|||
// Account Numbers
|
||||
'account_numbers' => [
|
||||
'V01PAK' => $this->getConfigValue('dhl_account_v01pak', config('dhl.account_numbers.V01PAK'), $useEnvPriority),
|
||||
'V62WP' => $this->getConfigValue('dhl_account_v62wp', config('dhl.account_numbers.V62WP'), $useEnvPriority),
|
||||
'V62KP' => $this->getConfigValue('dhl_account_v62kp', $this->getConfigValue('dhl_account_v62wp', config('dhl.account_numbers.V62KP'), $useEnvPriority), $useEnvPriority),
|
||||
'V53PAK' => $this->getConfigValue('dhl_account_v53pak', config('dhl.account_numbers.V53PAK'), $useEnvPriority),
|
||||
'V07PAK' => $this->getConfigValue('dhl_account_v07pak', config('dhl.account_numbers.V07PAK'), $useEnvPriority),
|
||||
'default' => config('dhl.account_numbers.default'),
|
||||
|
|
@ -103,7 +107,7 @@ class SettingController extends Controller
|
|||
// Dimensions
|
||||
'dimensions' => [
|
||||
'V01PAK' => config('dhl.dimensions.V01PAK'),
|
||||
'V62WP' => config('dhl.dimensions.V62WP'),
|
||||
'V62KP' => config('dhl.dimensions.V62KP'),
|
||||
'V53PAK' => config('dhl.dimensions.V53PAK'),
|
||||
'V07PAK' => config('dhl.dimensions.V07PAK'),
|
||||
'default' => config('dhl.dimensions.default'),
|
||||
|
|
@ -136,6 +140,26 @@ class SettingController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
private function normalizeDhlProductCode(?string $productCode): string
|
||||
{
|
||||
return $productCode === 'V62WP' ? 'V62KP' : ($productCode ?: 'V01PAK');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getDhlInternationalCountries(bool $useEnvPriority): array
|
||||
{
|
||||
$configCountries = config('dhl.international_countries', DhlProductResolver::DEFAULT_INTERNATIONAL_COUNTRIES);
|
||||
$countries = $configCountries;
|
||||
|
||||
if (! $useEnvPriority) {
|
||||
$countries = Setting::getContentBySlug('dhl_international_countries') ?: $configCountries;
|
||||
}
|
||||
|
||||
return DhlProductResolver::normalizeCountryCodeList(is_array($countries) ? $countries : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DHL configuration cache after saving settings
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\DhlShipment;
|
||||
use App\Services\DhlApiService;
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use App\Services\DhlTrackingService;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable as BusQueueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -14,7 +15,7 @@ use Illuminate\Support\Facades\Log;
|
|||
|
||||
/**
|
||||
* Job to track DHL shipments asynchronously
|
||||
*
|
||||
*
|
||||
* This job handles the tracking of DHL shipments in the background,
|
||||
* updating tracking status and details automatically.
|
||||
*/
|
||||
|
|
@ -37,7 +38,7 @@ class TrackShipmentJob implements ShouldQueue
|
|||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 2; // Lower tries for tracking as it's less critical
|
||||
public $tries = 2;
|
||||
|
||||
/**
|
||||
* The maximum number of seconds the job can run before timing out.
|
||||
|
|
@ -49,14 +50,13 @@ class TrackShipmentJob implements ShouldQueue
|
|||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param DhlShipment $dhlShipment
|
||||
* @param array $options
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(DhlShipment $dhlShipment, array $options = [])
|
||||
{
|
||||
$this->dhlShipment = $dhlShipment;
|
||||
$this->options = $options;
|
||||
|
||||
|
||||
// Set queue name - tracking is usually lower priority
|
||||
$this->onQueue('dhl-tracking');
|
||||
}
|
||||
|
|
@ -64,35 +64,34 @@ class TrackShipmentJob implements ShouldQueue
|
|||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
public function handle(DhlTrackingService $trackingService): void
|
||||
{
|
||||
try {
|
||||
Log::info('[DHL Queue] Starting shipment tracking job', [
|
||||
'shipment_id' => $this->dhlShipment->id,
|
||||
'tracking_number' => $this->dhlShipment->tracking_number,
|
||||
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
|
||||
'attempt' => $this->attempts(),
|
||||
]);
|
||||
|
||||
$dhlService = new DhlApiService();
|
||||
|
||||
// Get tracking details
|
||||
$trackingDetails = $dhlService->getTrackingDetails($this->dhlShipment);
|
||||
$result = $trackingService->updateTrackingNow($this->dhlShipment, $this->options);
|
||||
|
||||
Log::info('[DHL Queue] Shipment tracking updated successfully', [
|
||||
'shipment_id' => $this->dhlShipment->id,
|
||||
'tracking_status' => $trackingDetails['status'] ?? 'unknown',
|
||||
'events_count' => isset($trackingDetails['events']) ? count($trackingDetails['events']) : 0,
|
||||
'success' => $result['success'] ?? false,
|
||||
'tracking_status' => $result['tracking_status'] ?? 'unknown',
|
||||
'tracking_completed' => $result['tracking_completed'] ?? false,
|
||||
]);
|
||||
|
||||
// Schedule next tracking update if shipment is still in transit
|
||||
if (isset($this->options['auto_retrack']) && $this->options['auto_retrack']) {
|
||||
$status = $trackingDetails['status'] ?? '';
|
||||
if (($this->options['auto_retrack'] ?? false) && ! ($result['tracking_completed'] ?? false)) {
|
||||
$this->dhlShipment->refresh();
|
||||
$status = $this->dhlShipment->status ?? '';
|
||||
if ($this->shouldContinueTracking($status)) {
|
||||
// Schedule next tracking in 2-6 hours based on current status
|
||||
$nextTrackingDelay = $this->getNextTrackingDelay($status);
|
||||
TrackShipmentJob::dispatch($this->dhlShipment, $this->options)
|
||||
->delay(now()->addMinutes($nextTrackingDelay));
|
||||
|
||||
|
||||
Log::info('[DHL Queue] Next tracking job scheduled', [
|
||||
'shipment_id' => $this->dhlShipment->id,
|
||||
'delay_minutes' => $nextTrackingDelay,
|
||||
|
|
@ -103,7 +102,7 @@ class TrackShipmentJob implements ShouldQueue
|
|||
} catch (Exception $e) {
|
||||
Log::warning('[DHL Queue] Shipment tracking failed', [
|
||||
'shipment_id' => $this->dhlShipment->id,
|
||||
'tracking_number' => $this->dhlShipment->tracking_number,
|
||||
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
|
||||
'error' => $e->getMessage(),
|
||||
'attempt' => $this->attempts(),
|
||||
'max_tries' => $this->tries,
|
||||
|
|
@ -115,7 +114,7 @@ class TrackShipmentJob implements ShouldQueue
|
|||
'shipment_id' => $this->dhlShipment->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
|
||||
// Don't re-throw for final attempt - just log and continue
|
||||
return;
|
||||
}
|
||||
|
|
@ -126,14 +125,12 @@ class TrackShipmentJob implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Handle a job failure.
|
||||
*
|
||||
* @param Exception $exception
|
||||
*/
|
||||
public function failed(Exception $exception): void
|
||||
{
|
||||
Log::warning('[DHL Queue] TrackShipmentJob permanently failed', [
|
||||
'shipment_id' => $this->dhlShipment->id,
|
||||
'tracking_number' => $this->dhlShipment->tracking_number,
|
||||
'dhl_shipment_no' => $this->dhlShipment->dhl_shipment_no,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
|
|
@ -142,29 +139,14 @@ class TrackShipmentJob implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Determine if we should continue tracking this shipment
|
||||
*
|
||||
* @param string $status
|
||||
* @return bool
|
||||
*/
|
||||
private function shouldContinueTracking(string $status): bool
|
||||
{
|
||||
$finalStates = [
|
||||
'delivered',
|
||||
'delivered_to_recipient',
|
||||
'delivered_to_pickup_location',
|
||||
'returned_to_sender',
|
||||
'cancelled',
|
||||
'lost',
|
||||
];
|
||||
|
||||
return !in_array(strtolower($status), $finalStates);
|
||||
return ! in_array(strtolower($status), DhlShipment::TERMINAL_STATUSES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delay for next tracking update based on current status
|
||||
*
|
||||
* @param string $status
|
||||
* @return int Minutes until next tracking
|
||||
*/
|
||||
private function getNextTrackingDelay(string $status): int
|
||||
{
|
||||
|
|
@ -184,10 +166,8 @@ class TrackShipmentJob implements ShouldQueue
|
|||
|
||||
/**
|
||||
* Determine the time at which the job should timeout.
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function retryUntil()
|
||||
public function retryUntil(): DateTime
|
||||
{
|
||||
return now()->addMinutes(30); // Short timeout for tracking
|
||||
}
|
||||
|
|
|
|||
258
app/Services/DhlAddressValidator.php
Normal file
258
app/Services/DhlAddressValidator.php
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class DhlAddressValidator
|
||||
{
|
||||
private const DACH_COUNTRIES = ['DE', 'AT', 'CH'];
|
||||
|
||||
private const COUNTRY_SPECIFIC_POSTAL_PATTERNS = [
|
||||
'DE' => ['pattern' => '/^\d{5}$/', 'message' => 'Deutsche Postleitzahl muss 5 Ziffern haben.'],
|
||||
'AT' => ['pattern' => '/^\d{4}$/', 'message' => 'Oesterreichische Postleitzahl muss 4 Ziffern haben.'],
|
||||
'CH' => ['pattern' => '/^\d{4}$/', 'message' => 'Schweizer Postleitzahl muss 4 Ziffern haben.'],
|
||||
'ES' => ['pattern' => '/^\d{5}$/', 'message' => 'Spanische Postleitzahl muss 5 Ziffern haben.'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array{status: string, can_create_label: bool, errors: array<int, string>, warnings: array<int, string>, normalized: array<string, mixed>, validation_available: bool, validation_level: string, validation_message: string}
|
||||
*/
|
||||
public function validate(array $address): array
|
||||
{
|
||||
$normalized = $this->normalize($address);
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
$validationAvailable = $this->hasDachValidation($normalized['country_code']);
|
||||
$validationLevel = $validationAvailable ? 'formal_dach' : 'basic';
|
||||
$validationMessage = $validationAvailable
|
||||
? 'Formale DACH-Pruefung aktiv: Pflichtfelder, PLZ-Format, Plausibilitaet und Packstation-Regeln werden geprueft. Eine echte Adressdatenbank-/DHL-Leitcodepruefung ist nicht angebunden.'
|
||||
: 'Fuer dieses Zielland ist aktuell nur eine Basis-Adresspruefung verfuegbar.';
|
||||
|
||||
foreach ($this->requiredFields($normalized) as $field => $label) {
|
||||
if ($normalized[$field] === '') {
|
||||
$errors[] = "{$label} ist erforderlich.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($normalized['firstname'] === '' && $normalized['lastname'] === '' && $normalized['company'] === '') {
|
||||
$errors[] = 'Entweder Name oder Firmenname muss angegeben werden.';
|
||||
}
|
||||
|
||||
$this->validateCountryAndPostalCode($normalized, $errors, $warnings);
|
||||
$this->validateAddressPlausibility($normalized, $errors);
|
||||
$this->validatePackstation($normalized, $errors, $warnings);
|
||||
$this->validateWarnings($normalized, $warnings);
|
||||
|
||||
$status = 'valid';
|
||||
if ($errors !== []) {
|
||||
$status = 'error';
|
||||
} elseif ($warnings !== []) {
|
||||
$status = 'warning';
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'can_create_label' => $errors === [],
|
||||
'errors' => array_values(array_unique($errors)),
|
||||
'warnings' => array_values(array_unique($warnings)),
|
||||
'normalized' => $normalized,
|
||||
'validation_available' => $validationAvailable,
|
||||
'validation_level' => $validationLevel,
|
||||
'validation_message' => $validationMessage,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function normalize(array $address): array
|
||||
{
|
||||
$countryCode = $address['country_code']
|
||||
?? $address['shipping_country_code']
|
||||
?? data_get($address, 'country.code')
|
||||
?? '';
|
||||
|
||||
return [
|
||||
'firstname' => trim((string) ($address['firstname'] ?? $address['shipping_firstname'] ?? '')),
|
||||
'lastname' => trim((string) ($address['lastname'] ?? $address['shipping_lastname'] ?? '')),
|
||||
'company' => trim((string) ($address['company'] ?? $address['shipping_company'] ?? '')),
|
||||
'street' => trim((string) ($address['address'] ?? $address['street'] ?? $address['shipping_address'] ?? '')),
|
||||
'house_number' => trim((string) ($address['houseNumber'] ?? $address['house_number'] ?? $address['shipping_houseNumber'] ?? '')),
|
||||
'postal_code' => trim((string) ($address['zipcode'] ?? $address['postalCode'] ?? $address['postal_code'] ?? $address['shipping_zipcode'] ?? '')),
|
||||
'city' => trim((string) ($address['city'] ?? $address['shipping_city'] ?? '')),
|
||||
'country_code' => strtoupper(trim((string) $countryCode)),
|
||||
'email' => trim((string) ($address['email'] ?? $address['shipping_email'] ?? '')),
|
||||
'phone' => trim((string) ($address['phone'] ?? $address['shipping_phone'] ?? '')),
|
||||
'postnumber' => trim((string) ($address['postnumber'] ?? $address['postNumber'] ?? $address['shipping_postnumber'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function requiredFields(array $address): array
|
||||
{
|
||||
$fields = [
|
||||
'street' => 'Straße',
|
||||
'postal_code' => 'Postleitzahl',
|
||||
'city' => 'Ort',
|
||||
'country_code' => 'Land',
|
||||
];
|
||||
|
||||
if (! $this->isPackstation($address)) {
|
||||
$fields['house_number'] = 'Hausnummer';
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private function validateCountryAndPostalCode(array $address, array &$errors, array &$warnings): void
|
||||
{
|
||||
if ($address['country_code'] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$resolver = new DhlProductResolver;
|
||||
|
||||
try {
|
||||
if ($resolver->getAllowedProductCodesForCountry($address['country_code']) === []) {
|
||||
$errors[] = "DHL-Versand in das Zielland {$address['country_code']} ist aktuell nicht freigegeben.";
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset(self::COUNTRY_SPECIFIC_POSTAL_PATTERNS[$address['country_code']]) && $address['postal_code'] !== '') {
|
||||
$rule = self::COUNTRY_SPECIFIC_POSTAL_PATTERNS[$address['country_code']];
|
||||
if (! preg_match($rule['pattern'], $address['postal_code'])) {
|
||||
$errors[] = $rule['message'];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($address['postal_code'] !== '') {
|
||||
$warnings[] = 'Fuer dieses Zielland ist aktuell nur eine Basis-Adresspruefung verfuegbar. Bitte Adresse manuell pruefen.';
|
||||
}
|
||||
}
|
||||
|
||||
private function hasDachValidation(string $countryCode): bool
|
||||
{
|
||||
return in_array($countryCode, self::DACH_COUNTRIES, true);
|
||||
}
|
||||
|
||||
private function validateAddressPlausibility(array $address, array &$errors): void
|
||||
{
|
||||
$isDachAddress = $this->hasDachValidation($address['country_code']);
|
||||
|
||||
if ($address['street'] !== '' && mb_strlen($address['street']) < 3) {
|
||||
$errors[] = 'Straße ist zu kurz.';
|
||||
}
|
||||
|
||||
if ($address['street'] !== '' && ! preg_match('/[a-zäöüß]/iu', $address['street'])) {
|
||||
$errors[] = 'Straße muss Buchstaben enthalten.';
|
||||
}
|
||||
|
||||
if ($address['city'] !== '' && mb_strlen($address['city']) < 2) {
|
||||
$errors[] = 'Ort ist zu kurz.';
|
||||
}
|
||||
|
||||
if ($address['city'] !== '' && ! preg_match('/[a-zäöüß]/iu', $address['city'])) {
|
||||
$errors[] = 'Ort muss Buchstaben enthalten.';
|
||||
}
|
||||
|
||||
if ($address['postal_code'] !== '' && ! preg_match('/^[A-Z0-9][A-Z0-9 -]{2,9}$/i', $address['postal_code'])) {
|
||||
$errors[] = 'Postleitzahl enthaelt ungueltige Zeichen.';
|
||||
}
|
||||
|
||||
if ($isDachAddress && ! $this->isPackstation($address) && ! preg_match('/\d/', $address['house_number'])) {
|
||||
$errors[] = 'Hausnummer muss fuer DACH-Adressen eine Ziffer enthalten.';
|
||||
}
|
||||
|
||||
if ($this->containsPlaceholderValue($address['street'])) {
|
||||
$errors[] = 'Straße wirkt wie eine Test- oder Platzhalteradresse.';
|
||||
}
|
||||
|
||||
if ($this->containsPlaceholderValue($address['city'])) {
|
||||
$errors[] = 'Ort wirkt wie eine Test- oder Platzhalterangabe.';
|
||||
}
|
||||
}
|
||||
|
||||
private function validatePackstation(array $address, array &$errors, array &$warnings): void
|
||||
{
|
||||
if ($this->isPackstation($address) && $address['postnumber'] === '') {
|
||||
$errors[] = 'DHL Postnummer ist fuer Packstation/Paketbox erforderlich.';
|
||||
}
|
||||
|
||||
if ($address['postnumber'] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($address['country_code'] !== 'DE') {
|
||||
$errors[] = 'Packstation/Paketbox ist aktuell nur fuer Deutschland erlaubt.';
|
||||
}
|
||||
|
||||
if (! preg_match('/^[0-9]{6,10}$/', $address['postnumber'])) {
|
||||
$errors[] = 'DHL Postnummer muss 6-10 Ziffern enthalten.';
|
||||
}
|
||||
|
||||
if (! $this->isPackstation($address)) {
|
||||
$warnings[] = 'DHL Postnummer ist gesetzt. Bitte pruefen, ob Straße/Nr. eine Packstation oder Paketbox enthaelt.';
|
||||
}
|
||||
|
||||
$lockerNumber = $this->extractLockerNumber($address);
|
||||
if ($this->isPackstation($address) && $lockerNumber === null) {
|
||||
$errors[] = 'Packstation-/Paketbox-Nummer fehlt.';
|
||||
}
|
||||
|
||||
if ($lockerNumber !== null && ((int) $lockerNumber < 100 || (int) $lockerNumber > 999)) {
|
||||
$errors[] = 'Packstation-/Paketbox-Nummer muss 3-stellig sein (100-999).';
|
||||
}
|
||||
}
|
||||
|
||||
private function validateWarnings(array $address, array &$warnings): void
|
||||
{
|
||||
if ($address['phone'] === '') {
|
||||
$warnings[] = 'Telefonnummer fehlt. DHL kann Empfaenger bei Zustellproblemen eventuell nicht kontaktieren.';
|
||||
}
|
||||
|
||||
if ($address['email'] === '') {
|
||||
$warnings[] = 'E-Mail-Adresse fehlt. Tracking-Mails koennen nicht automatisch an diese Adresse gesendet werden.';
|
||||
}
|
||||
|
||||
if (! $this->hasDachValidation($address['country_code']) && ! $this->isPackstation($address) && ! preg_match('/\d/', $address['house_number'])) {
|
||||
$warnings[] = 'Hausnummer enthaelt keine Ziffer. Bitte Adresse manuell pruefen.';
|
||||
}
|
||||
}
|
||||
|
||||
private function containsPlaceholderValue(string $value): bool
|
||||
{
|
||||
$value = mb_strtolower(trim($value));
|
||||
|
||||
if ($value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) preg_match('/(test|fake|falsch|dummy|asdf|qwertz|qwerty|xxx|kein|ungueltig|invalid)/u', $value)
|
||||
|| preg_match('/^(.)\1{2,}$/u', $value);
|
||||
}
|
||||
|
||||
private function isPackstation(array $address): bool
|
||||
{
|
||||
return (bool) preg_match('/\b(packstation|paketbox)\b/i', $address['street'].' '.$address['house_number']);
|
||||
}
|
||||
|
||||
private function extractLockerNumber(array $address): ?string
|
||||
{
|
||||
if (preg_match('/(?:packstation|paketbox)\s*(\d+)/i', $address['street'], $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if ($this->isPackstation($address) && preg_match('/\d+/', $address['house_number'], $matches)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,15 +35,27 @@ class DhlDataHelper
|
|||
$settingController = new SettingController;
|
||||
$dhlConfig = $settingController->getDhlConfig();
|
||||
}
|
||||
$dimensions = isset($dhlConfig['dimensions'][$options['product_code']]) ? $dhlConfig['dimensions'][$options['product_code']] : $dhlConfig['dimensions']['default'];
|
||||
$resolver = new DhlProductResolver;
|
||||
$destinationCountryCode = $shippingAddress['country']?->code;
|
||||
if (! $destinationCountryCode) {
|
||||
throw new \Exception('shipping_address.country is required');
|
||||
}
|
||||
|
||||
$resolvedDhlProduct = $resolver->resolveForShipment(
|
||||
$destinationCountryCode,
|
||||
$options['product_code'] ?? null,
|
||||
$dhlConfig['default_product'] ?? 'V01PAK'
|
||||
);
|
||||
$dimensions = $dhlConfig['dimensions'][$resolvedDhlProduct['product_code']] ?? $dhlConfig['dimensions']['default'];
|
||||
|
||||
return [
|
||||
'order_id' => $order->id,
|
||||
'weight_kg' => $weight,
|
||||
'product_code' => $options['product_code'] ?? 'V01PAK',
|
||||
'product_code' => $resolvedDhlProduct['product_code'],
|
||||
'label_format' => $options['label_format'] ?? $dhlConfig['label_format'] ?? 'PDF',
|
||||
'print_format' => $options['print_format'] ?? $dhlConfig['print_format'] ?? null,
|
||||
'retoure_print_format' => $options['retoure_print_format'] ?? $dhlConfig['retoure_print_format'] ?? null,
|
||||
'print_only_if_codeable' => (bool) ($options['print_only_if_codeable'] ?? $dhlConfig['print_only_if_codeable'] ?? config('dhl.print_only_if_codeable', true)),
|
||||
|
||||
// Shipper data (sender) - from admin settings
|
||||
'shipper' => [
|
||||
|
|
@ -66,7 +78,7 @@ class DhlDataHelper
|
|||
'houseNumber' => $shippingAddress['houseNumber'] ?? '',
|
||||
'postalCode' => $shippingAddress['zipcode'] ?? '',
|
||||
'city' => $shippingAddress['city'] ?? '',
|
||||
'country' => $shippingAddress['country']?->code ?? 'DE',
|
||||
'country' => $resolvedDhlProduct['country_code'],
|
||||
'email' => $shippingAddress['email'] ?? '',
|
||||
'phone' => $shippingAddress['phone'] ?? '',
|
||||
// DHL Postnummer für Packstation/Paketbox
|
||||
|
|
@ -82,7 +94,18 @@ class DhlDataHelper
|
|||
'services' => $options['services'] ?? [],
|
||||
|
||||
// Custom reference for tracking
|
||||
'reference' => 'Order-'.$order->id,
|
||||
'reference' => self::normalizeReference($options['reference'] ?? $options['shipment_reference'] ?? null, $order),
|
||||
];
|
||||
}
|
||||
|
||||
private static function normalizeReference(?string $reference, ShoppingOrder $order): string
|
||||
{
|
||||
$reference = trim((string) $reference);
|
||||
|
||||
if ($reference === '') {
|
||||
return 'Order-'.$order->id;
|
||||
}
|
||||
|
||||
return mb_substr(preg_replace('/\s+/', ' ', $reference), 0, 35);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ class DhlModalService
|
|||
'shippingAddress' => null,
|
||||
'availableCountries' => $this->getAvailableCountries(),
|
||||
'productCodes' => $this->getAvailableProductCodes(),
|
||||
'productSuggestions' => (new DhlProductResolver)->getProductSuggestionsByCountry(),
|
||||
'selectedProductCode' => null,
|
||||
'errors' => [],
|
||||
'warnings' => [],
|
||||
'existingShipments' => [],
|
||||
|
|
@ -89,6 +91,7 @@ class DhlModalService
|
|||
|
||||
// Process and validate shipping address
|
||||
$result['shippingAddress'] = $this->processShippingAddress($order);
|
||||
$result['selectedProductCode'] = $this->getSuggestedProductCode($result['shippingAddress']);
|
||||
|
||||
// Validate address completeness
|
||||
$addressValidation = $this->validateAddress($result['shippingAddress']);
|
||||
|
|
@ -111,7 +114,7 @@ class DhlModalService
|
|||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$result['errors'][] = 'Fehler beim Laden der Bestelldaten: ' . $e->getMessage();
|
||||
$result['errors'][] = 'Fehler beim Laden der Bestelldaten: '.$e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
|
@ -125,7 +128,7 @@ class DhlModalService
|
|||
private function loadOrder($id): ?ShoppingOrder
|
||||
{
|
||||
return ShoppingOrder::with([
|
||||
'shopping_order_items',
|
||||
'shopping_order_items.product',
|
||||
'shopping_user',
|
||||
'dhlShipments', // Include DHL shipments
|
||||
])->find($id);
|
||||
|
|
@ -170,38 +173,7 @@ class DhlModalService
|
|||
*/
|
||||
private function calculateOrderWeight(ShoppingOrder $order): float
|
||||
{
|
||||
return $order->weight / 1000; // from grams to kg
|
||||
/*
|
||||
// Default fallback weight
|
||||
$defaultWeight = 1.0;
|
||||
|
||||
if (!$order->shopping_order_items || $order->shopping_order_items->isEmpty()) {
|
||||
return $defaultWeight;
|
||||
}
|
||||
|
||||
// If order has a weight field (in grams), convert to kg
|
||||
if ($order->weight && $order->weight > 0) {
|
||||
return round($order->weight / 100, 1); // Convert grams to kg
|
||||
}
|
||||
|
||||
// Calculate from items if available
|
||||
$totalWeight = 0;
|
||||
foreach ($order->shopping_order_items as $item) {
|
||||
if ($item->weight && $item->weight > 0) {
|
||||
$totalWeight += ($item->weight * $item->quantity);
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalWeight > 0) {
|
||||
return round($totalWeight / 100, 1); // Convert grams to kg
|
||||
}
|
||||
|
||||
// Estimate based on item count if no weight data
|
||||
$itemCount = $order->shopping_order_items->sum('quantity');
|
||||
$estimatedWeight = max($itemCount * 0.5, $defaultWeight); // Estimate 0.5kg per item
|
||||
|
||||
return round($estimatedWeight, 1);
|
||||
*/
|
||||
return (new DhlShipmentWeightCalculator)->calculate($order);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -287,50 +259,12 @@ class DhlModalService
|
|||
*/
|
||||
private function validateAddress(array $address): array
|
||||
{
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
// Required fields
|
||||
$requiredFields = [
|
||||
'firstname' => 'Vorname',
|
||||
'lastname' => 'Nachname',
|
||||
'address' => 'Straße',
|
||||
'zipcode' => 'Postleitzahl',
|
||||
'city' => 'Stadt',
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field => $label) {
|
||||
if (empty(trim($address[$field]))) {
|
||||
$errors[] = "{$label} ist erforderlich.";
|
||||
}
|
||||
}
|
||||
|
||||
// Name validation
|
||||
if (empty(trim($address['firstname'])) && empty(trim($address['lastname'])) && empty(trim($address['company']))) {
|
||||
$errors[] = 'Entweder Name oder Firmenname muss angegeben werden.';
|
||||
}
|
||||
|
||||
// Street number validation
|
||||
if (! empty($address['address']) && empty($address['houseNumber'])) {
|
||||
$warnings[] = 'Hausnummer konnte nicht automatisch erkannt werden. Bitte prüfen Sie die Adressangaben.';
|
||||
}
|
||||
|
||||
// Postal code format validation for Germany
|
||||
if (! empty($address['zipcode']) && $address['country'] && $address['country']->code === 'DE') {
|
||||
if (! preg_match('/^\d{5}$/', $address['zipcode'])) {
|
||||
$warnings[] = 'Deutsche Postleitzahl sollte 5 Ziffern haben.';
|
||||
}
|
||||
}
|
||||
|
||||
// Country validation
|
||||
if (! $address['country']) {
|
||||
$errors[] = 'Land konnte nicht ermittelt werden.';
|
||||
}
|
||||
$result = (new DhlAddressValidator)->validate($address);
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
'warnings' => $warnings,
|
||||
'valid' => $result['can_create_label'],
|
||||
'errors' => $result['errors'],
|
||||
'warnings' => $result['warnings'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -386,8 +320,8 @@ class DhlModalService
|
|||
$productCodes['V53PAK'] = 'DHL Paket International';
|
||||
}
|
||||
|
||||
if (! empty($accountNumbers['V62WP'])) {
|
||||
$productCodes['V62WP'] = 'DHL Warenpost National';
|
||||
if (! empty($accountNumbers['V62KP'])) {
|
||||
$productCodes['V62KP'] = 'DHL Kleinpaket';
|
||||
}
|
||||
|
||||
if (! empty($accountNumbers['V07PAK'])) {
|
||||
|
|
@ -399,13 +333,27 @@ class DhlModalService
|
|||
$productCodes = [
|
||||
'V01PAK' => 'DHL Paket National',
|
||||
'V53PAK' => 'DHL Paket International',
|
||||
'V62WP' => 'DHL Warenpost National',
|
||||
'V62KP' => 'DHL Kleinpaket',
|
||||
];
|
||||
}
|
||||
|
||||
return $productCodes;
|
||||
}
|
||||
|
||||
private function getSuggestedProductCode(array $shippingAddress): string
|
||||
{
|
||||
$countryCode = $shippingAddress['country']?->code;
|
||||
if (! $countryCode) {
|
||||
return 'V01PAK';
|
||||
}
|
||||
|
||||
try {
|
||||
return (new DhlProductResolver)->resolveProductCode($countryCode, null, 'V01PAK');
|
||||
} catch (\InvalidArgumentException) {
|
||||
return 'V01PAK';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate shipment parameters before API call
|
||||
*
|
||||
|
|
@ -431,23 +379,35 @@ class DhlModalService
|
|||
$errors[] = 'Ungültiger Produktcode ausgewählt.';
|
||||
}
|
||||
|
||||
// Address validation
|
||||
$requiredAddressFields = [
|
||||
'shipping_firstname' => 'Vorname',
|
||||
'shipping_lastname' => 'Nachname',
|
||||
'shipping_address' => 'Straße',
|
||||
'shipping_houseNumber' => 'Hausnummer',
|
||||
'shipping_zipcode' => 'Postleitzahl',
|
||||
'shipping_city' => 'Stadt',
|
||||
'shipping_country_id' => 'Land',
|
||||
];
|
||||
|
||||
foreach ($requiredAddressFields as $field => $label) {
|
||||
if (empty(trim($shipmentData[$field] ?? ''))) {
|
||||
$errors[] = "{$label} ist erforderlich.";
|
||||
if (! empty($shipmentData['shipping_country_id']) && $productCode) {
|
||||
$country = Country::find($shipmentData['shipping_country_id']);
|
||||
if ($country) {
|
||||
try {
|
||||
(new DhlProductResolver)->resolveProductCode($country->code, $productCode);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($productCode) {
|
||||
try {
|
||||
(new DhlShipmentWeightCalculator)->assertWithinProductLimit($weight, $productCode);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$country = null;
|
||||
if (! empty($shipmentData['shipping_country_id'])) {
|
||||
$country = Country::find($shipmentData['shipping_country_id']);
|
||||
}
|
||||
$addressValidation = (new DhlAddressValidator)->validate(array_merge($shipmentData, [
|
||||
'shipping_country_code' => $country?->code,
|
||||
]));
|
||||
$errors = array_merge($errors, $addressValidation['errors']);
|
||||
$warnings = array_merge($warnings, $addressValidation['warnings']);
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
|
|
|
|||
199
app/Services/DhlProductResolver.php
Normal file
199
app/Services/DhlProductResolver.php
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DhlProductResolver
|
||||
{
|
||||
public const DOMESTIC_COUNTRY = 'DE';
|
||||
|
||||
public const DOMESTIC_PRODUCT_CODES = ['V01PAK', 'V62KP'];
|
||||
|
||||
public const INTERNATIONAL_PRODUCT_CODE = 'V53PAK';
|
||||
|
||||
public const DEFAULT_INTERNATIONAL_COUNTRIES = ['AT', 'ES'];
|
||||
|
||||
public const DHL_COUNTRY_CODES = [
|
||||
'DE' => 'DEU',
|
||||
'AT' => 'AUT',
|
||||
'ES' => 'ESP',
|
||||
'CH' => 'CHE',
|
||||
'US' => 'USA',
|
||||
'GB' => 'GBR',
|
||||
'FR' => 'FRA',
|
||||
'IT' => 'ITA',
|
||||
'NL' => 'NLD',
|
||||
'BE' => 'BEL',
|
||||
'PL' => 'POL',
|
||||
'CZ' => 'CZE',
|
||||
'DK' => 'DNK',
|
||||
'SE' => 'SWE',
|
||||
'NO' => 'NOR',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array{country_code: string, dhl_country_code: string, product_code: string}
|
||||
*/
|
||||
public function resolveForShipment(string $destinationCountryCode, ?string $requestedProductCode = null, ?string $defaultProductCode = null): array
|
||||
{
|
||||
$countryCode = $this->normalizeCountryCode($destinationCountryCode);
|
||||
$productCode = $this->resolveProductCode($countryCode, $requestedProductCode, $defaultProductCode);
|
||||
|
||||
return [
|
||||
'country_code' => $countryCode,
|
||||
'dhl_country_code' => $this->toDhlCountryCode($countryCode),
|
||||
'product_code' => $productCode,
|
||||
];
|
||||
}
|
||||
|
||||
public function resolveProductCode(string $destinationCountryCode, ?string $requestedProductCode = null, ?string $defaultProductCode = null): string
|
||||
{
|
||||
$countryCode = $this->normalizeCountryCode($destinationCountryCode);
|
||||
$hasRequestedProduct = $requestedProductCode !== null && trim($requestedProductCode) !== '';
|
||||
$productCode = $this->normalizeProductCode($requestedProductCode ?: $defaultProductCode);
|
||||
|
||||
if ($countryCode === self::DOMESTIC_COUNTRY) {
|
||||
$productCode = $productCode ?: 'V01PAK';
|
||||
|
||||
if (! in_array($productCode, self::DOMESTIC_PRODUCT_CODES, true)) {
|
||||
throw new InvalidArgumentException("Produkt {$productCode} ist fuer DHL-Versand nach Deutschland nicht erlaubt.");
|
||||
}
|
||||
|
||||
return $productCode;
|
||||
}
|
||||
|
||||
if (! in_array($countryCode, $this->getSupportedInternationalCountries(), true)) {
|
||||
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} ist aktuell nicht freigegeben.");
|
||||
}
|
||||
|
||||
if (! $productCode || (! $hasRequestedProduct && in_array($productCode, self::DOMESTIC_PRODUCT_CODES, true))) {
|
||||
return self::INTERNATIONAL_PRODUCT_CODE;
|
||||
}
|
||||
|
||||
if ($productCode !== self::INTERNATIONAL_PRODUCT_CODE) {
|
||||
throw new InvalidArgumentException("Produkt {$productCode} ist fuer DHL-Versand in das Zielland {$countryCode} nicht erlaubt.");
|
||||
}
|
||||
|
||||
return $productCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getProductSuggestionsByCountry(): array
|
||||
{
|
||||
return array_fill_keys($this->getSupportedInternationalCountries(), self::INTERNATIONAL_PRODUCT_CODE)
|
||||
+ [self::DOMESTIC_COUNTRY => 'V01PAK'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllowedProductCodesForCountry(string $destinationCountryCode): array
|
||||
{
|
||||
$countryCode = $this->normalizeCountryCode($destinationCountryCode);
|
||||
|
||||
if ($countryCode === self::DOMESTIC_COUNTRY) {
|
||||
return self::DOMESTIC_PRODUCT_CODES;
|
||||
}
|
||||
|
||||
if (in_array($countryCode, $this->getSupportedInternationalCountries(), true)) {
|
||||
return [self::INTERNATIONAL_PRODUCT_CODE];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function toDhlCountryCode(string $countryCode): string
|
||||
{
|
||||
$countryCode = $this->normalizeCountryCode($countryCode);
|
||||
|
||||
return self::DHL_COUNTRY_CODES[$countryCode];
|
||||
}
|
||||
|
||||
public function assertBillingNumber(string $productCode, ?string $billingNumber): string
|
||||
{
|
||||
if ($billingNumber === null || trim($billingNumber) === '') {
|
||||
throw new InvalidArgumentException("Keine DHL-Abrechnungsnummer fuer Produkt {$productCode} konfiguriert.");
|
||||
}
|
||||
|
||||
return $billingNumber;
|
||||
}
|
||||
|
||||
public function getProductScope(string $productCode): string
|
||||
{
|
||||
$productCode = $this->normalizeProductCode($productCode);
|
||||
|
||||
return $productCode === self::INTERNATIONAL_PRODUCT_CODE ? 'international' : 'national';
|
||||
}
|
||||
|
||||
public function getProductScopeLabel(string $productCode): string
|
||||
{
|
||||
return $this->getProductScope($productCode) === 'international'
|
||||
? 'DHL Paket International'
|
||||
: 'DHL Paket National';
|
||||
}
|
||||
|
||||
public function normalizeCountryCode(string $countryCode): string
|
||||
{
|
||||
$countryCode = strtoupper(trim($countryCode));
|
||||
|
||||
if ($countryCode === '') {
|
||||
throw new InvalidArgumentException('DHL-Zielland fehlt.');
|
||||
}
|
||||
|
||||
if (strlen($countryCode) === 3) {
|
||||
$countryCode = array_search($countryCode, self::DHL_COUNTRY_CODES, true) ?: $countryCode;
|
||||
}
|
||||
|
||||
if (! array_key_exists($countryCode, self::DHL_COUNTRY_CODES)) {
|
||||
throw new InvalidArgumentException("DHL-Laendercode {$countryCode} wird nicht unterstuetzt.");
|
||||
}
|
||||
|
||||
return $countryCode;
|
||||
}
|
||||
|
||||
public function normalizeProductCode(?string $productCode): ?string
|
||||
{
|
||||
if ($productCode === null || trim($productCode) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return strtoupper(trim($productCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSupportedInternationalCountries(): array
|
||||
{
|
||||
$useEnvPriority = config('dhl.config_source') === 'env';
|
||||
$configCountries = config('dhl.international_countries', self::DEFAULT_INTERNATIONAL_COUNTRIES);
|
||||
$countries = $configCountries;
|
||||
|
||||
if (! $useEnvPriority) {
|
||||
$countries = Setting::getContentBySlug('dhl_international_countries') ?: $configCountries;
|
||||
}
|
||||
|
||||
return self::normalizeCountryCodeList(is_array($countries) ? $countries : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function normalizeCountryCodeList(array $countryCodes): array
|
||||
{
|
||||
$countryCodes = array_map(
|
||||
static fn ($countryCode): string => strtoupper(trim((string) $countryCode)),
|
||||
$countryCodes
|
||||
);
|
||||
|
||||
return array_values(array_filter(array_unique($countryCodes), static function (string $countryCode): bool {
|
||||
return $countryCode !== ''
|
||||
&& $countryCode !== self::DOMESTIC_COUNTRY
|
||||
&& array_key_exists($countryCode, self::DHL_COUNTRY_CODES);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Acme\Dhl\Exceptions\DhlAddressValidationException;
|
||||
use Acme\Dhl\Models\DhlShipment;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Http\Controllers\SettingController;
|
||||
use App\Jobs\CreateShipmentJob;
|
||||
use App\Jobs\CancelShipmentJob;
|
||||
use App\Services\DhlDataHelper;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Jobs\CreateShipmentJob;
|
||||
use App\Models\ShoppingOrder;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* DHL Shipment Service
|
||||
*
|
||||
*
|
||||
* Handles both synchronous and asynchronous shipment creation
|
||||
* based on configuration settings
|
||||
*/
|
||||
|
|
@ -21,20 +21,23 @@ class DhlShipmentService
|
|||
{
|
||||
/**
|
||||
* Create a DHL shipment (sync or async based on config)
|
||||
*
|
||||
* @param ShoppingOrder $order
|
||||
* @param float $weight
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function createShipment(ShoppingOrder $order, float $weight = 1.0, array $options = []): array
|
||||
{
|
||||
$weight = max($weight, (new DhlShipmentWeightCalculator)->calculate($order));
|
||||
|
||||
// Get DHL configuration
|
||||
$settingController = new SettingController();
|
||||
$settingController = new SettingController;
|
||||
$dhlConfig = $settingController->getDhlConfig();
|
||||
\Log::info('dhlConfig', $dhlConfig);
|
||||
// Check if queue should be used
|
||||
$useQueue = $dhlConfig['use_queue'] ?? false;
|
||||
if ($useQueue && $this->requiresSynchronousAddressValidation($options, $dhlConfig)) {
|
||||
Log::info('[DHL Service] Queue disabled for DHL mustEncode address validation', [
|
||||
'order_id' => $order->id,
|
||||
]);
|
||||
$useQueue = false;
|
||||
}
|
||||
|
||||
if ($useQueue) {
|
||||
return $this->createShipmentAsync($order, $weight, $options, $dhlConfig);
|
||||
|
|
@ -43,14 +46,20 @@ class DhlShipmentService
|
|||
}
|
||||
}
|
||||
|
||||
private function requiresSynchronousAddressValidation(array $options, array $dhlConfig): bool
|
||||
{
|
||||
if (! (bool) ($options['print_only_if_codeable'] ?? $dhlConfig['print_only_if_codeable'] ?? config('dhl.print_only_if_codeable', true))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$country = $options['shipping_address']['country'] ?? null;
|
||||
$countryCode = is_object($country) ? ($country->code ?? null) : ($country['code'] ?? null);
|
||||
|
||||
return strtoupper((string) $countryCode) === DhlProductResolver::DOMESTIC_COUNTRY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shipment asynchronously using queue
|
||||
*
|
||||
* @param ShoppingOrder $order
|
||||
* @param float $weight
|
||||
* @param array $options
|
||||
* @param array $dhlConfig
|
||||
* @return array
|
||||
*/
|
||||
private function createShipmentAsync(ShoppingOrder $order, float $weight, array $options, array $dhlConfig): array
|
||||
{
|
||||
|
|
@ -60,14 +69,14 @@ class DhlShipmentService
|
|||
|
||||
Log::info('[DHL Service] Shipment creation dispatched to queue', [
|
||||
'order_id' => $order->id,
|
||||
'weight' => $weight
|
||||
'weight' => $weight,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Sendung wird erstellt. Sie erhalten eine Benachrichtigung, sobald das Versandlabel verfügbar ist.',
|
||||
'queued' => true,
|
||||
'order_id' => $order->id
|
||||
'order_id' => $order->id,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
Log::error('[DHL Service] Failed to dispatch shipment creation', [
|
||||
|
|
@ -77,27 +86,21 @@ class DhlShipmentService
|
|||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Fehler beim Einreihen der Sendungserstellung: ' . $e->getMessage(),
|
||||
'queued' => false
|
||||
'message' => 'Fehler beim Einreihen der Sendungserstellung: '.$e->getMessage(),
|
||||
'queued' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shipment synchronously
|
||||
*
|
||||
* @param ShoppingOrder $order
|
||||
* @param float $weight
|
||||
* @param array $options
|
||||
* @param array $dhlConfig
|
||||
* @return array
|
||||
*/
|
||||
private function createShipmentSync(ShoppingOrder $order, float $weight, array $options, array $dhlConfig): array
|
||||
{
|
||||
try {
|
||||
Log::info('[DHL Service] Creating shipment synchronously', [
|
||||
'order_id' => $order->id,
|
||||
'weight' => $weight
|
||||
'weight' => $weight,
|
||||
]);
|
||||
|
||||
// Create DHL client directly with correct base URL
|
||||
|
|
@ -132,32 +135,42 @@ class DhlShipmentService
|
|||
'label_path' => $result['labelPath'] ?? null,
|
||||
'label_url' => $result['labelUrl'] ?? null,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
Log::error('[DHL Service] Shipment creation failed (sync)', [
|
||||
} catch (DhlAddressValidationException $e) {
|
||||
Log::warning('[DHL Service] Shipment address validation failed (sync)', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Fehler beim Erstellen des Versandlabels: ' . $e->getMessage(),
|
||||
'type' => 'dhl_address_validation',
|
||||
'message' => $e->getMessage(),
|
||||
'errors' => [$e->getMessage()],
|
||||
'queued' => false,
|
||||
'order_id' => $order->id
|
||||
'order_id' => $order->id,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
Log::error('[DHL Service] Shipment creation failed (sync)', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Fehler beim Erstellen des Versandlabels: '.$e->getMessage(),
|
||||
'queued' => false,
|
||||
'order_id' => $order->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a DHL shipment (sync or async based on config)
|
||||
*
|
||||
* @param DhlShipment $shipment
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function cancelShipment(DhlShipment $shipment, array $options = []): array
|
||||
{
|
||||
// Get DHL configuration
|
||||
$settingController = new SettingController();
|
||||
$settingController = new SettingController;
|
||||
$dhlConfig = $settingController->getDhlConfig();
|
||||
|
||||
// Check if queue should be used
|
||||
|
|
@ -172,11 +185,6 @@ class DhlShipmentService
|
|||
|
||||
/**
|
||||
* Cancel shipment asynchronously using queue
|
||||
*
|
||||
* @param DhlShipment $shipment
|
||||
* @param array $options
|
||||
* @param array $dhlConfig
|
||||
* @return array
|
||||
*/
|
||||
private function cancelShipmentAsync(DhlShipment $shipment, array $options, array $dhlConfig): array
|
||||
{
|
||||
|
|
@ -186,14 +194,14 @@ class DhlShipmentService
|
|||
|
||||
Log::info('[DHL Service] Shipment cancellation dispatched to queue', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'dhl_shipment_no' => $shipment->dhl_shipment_no
|
||||
'dhl_shipment_no' => $shipment->dhl_shipment_no,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Sendung wird storniert...',
|
||||
'queued' => true,
|
||||
'shipment_id' => $shipment->id
|
||||
'shipment_id' => $shipment->id,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
Log::error('[DHL Service] Failed to dispatch shipment cancellation', [
|
||||
|
|
@ -203,40 +211,39 @@ class DhlShipmentService
|
|||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Fehler beim Einreihen der Stornierung: ' . $e->getMessage(),
|
||||
'queued' => false
|
||||
'message' => 'Fehler beim Einreihen der Stornierung: '.$e->getMessage(),
|
||||
'queued' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel shipment synchronously
|
||||
*
|
||||
* @param DhlShipment $shipment
|
||||
* @param array $options
|
||||
* @param array $dhlConfig
|
||||
* @return array
|
||||
*/
|
||||
private function cancelShipmentSync(DhlShipment $shipment, array $options, array $dhlConfig): array
|
||||
{
|
||||
try {
|
||||
// Validate shipment has DHL number
|
||||
if (empty($shipment->dhl_shipment_no)) {
|
||||
$this->recordCancellationFailure($shipment, 'missing_shipment_number', 'Sendung hat keine DHL-Sendungsnummer und kann nicht storniert werden.');
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Sendung hat keine DHL-Sendungsnummer und kann nicht storniert werden.',
|
||||
'queued' => false,
|
||||
'shipment_id' => $shipment->id
|
||||
'shipment_id' => $shipment->id,
|
||||
];
|
||||
}
|
||||
|
||||
// Validate shipment can be cancelled
|
||||
if (! $shipment->canCancel()) {
|
||||
$this->recordCancellationFailure($shipment, 'status_not_cancelable', 'Sendung kann im aktuellen Status "'.$shipment->status.'" nicht storniert werden.');
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Sendung kann im aktuellen Status "' . $shipment->status . '" nicht storniert werden. Nur Status "created" oder "pending" sind stornierbar.',
|
||||
'message' => 'Sendung kann im aktuellen Status "'.$shipment->getStatusTranslation().'" nicht storniert werden. Nur Status "Erstellt" oder "Wartend" sind stornierbar.',
|
||||
'queued' => false,
|
||||
'shipment_id' => $shipment->id
|
||||
'shipment_id' => $shipment->id,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +251,7 @@ class DhlShipmentService
|
|||
'shipment_id' => $shipment->id,
|
||||
'dhl_shipment_no' => $shipment->dhl_shipment_no,
|
||||
'status' => $shipment->status,
|
||||
'base_url' => $dhlConfig['base_url']
|
||||
'base_url' => $dhlConfig['base_url'],
|
||||
]);
|
||||
|
||||
// Create DHL client
|
||||
|
|
@ -263,37 +270,41 @@ class DhlShipmentService
|
|||
if ($success) {
|
||||
Log::info('[DHL Service] Shipment cancelled successfully (sync)', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'dhl_shipment_no' => $shipment->dhl_shipment_no
|
||||
'dhl_shipment_no' => $shipment->dhl_shipment_no,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Sendung wurde erfolgreich storniert!',
|
||||
'queued' => false,
|
||||
'shipment_id' => $shipment->id
|
||||
'shipment_id' => $shipment->id,
|
||||
];
|
||||
} else {
|
||||
throw new Exception('Cancellation returned false');
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->recordCancellationFailure($shipment, 'validation_failed', $e->getMessage(), $e);
|
||||
|
||||
Log::warning('[DHL Service] Shipment cancellation validation failed', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'queued' => false,
|
||||
'shipment_id' => $shipment->id
|
||||
'shipment_id' => $shipment->id,
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$this->recordCancellationFailure($shipment, 'api_failed', $e->getMessage(), $e);
|
||||
|
||||
Log::error('[DHL Service] Shipment cancellation failed (sync)', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'dhl_shipment_no' => $shipment->dhl_shipment_no,
|
||||
'status' => $shipment->status,
|
||||
'error' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString()
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
// Check if it's an API authentication/resource error
|
||||
|
|
@ -304,16 +315,50 @@ class DhlShipmentService
|
|||
'message' => 'Die Sendung konnte bei DHL nicht gefunden werden. Mögliche Ursachen: Sendung wurde bereits storniert, ist zu alt, oder wurde in einem anderen Modus (Sandbox/Production) erstellt.',
|
||||
'queued' => false,
|
||||
'shipment_id' => $shipment->id,
|
||||
'technical_error' => $errorMessage
|
||||
'technical_error' => $errorMessage,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Fehler beim Stornieren der Sendung: ' . $errorMessage,
|
||||
'message' => 'Fehler beim Stornieren der Sendung: '.$errorMessage,
|
||||
'queued' => false,
|
||||
'shipment_id' => $shipment->id
|
||||
'shipment_id' => $shipment->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function recordCancellationFailure(DhlShipment $shipment, string $reason, string $detail, ?Exception $exception = null): void
|
||||
{
|
||||
$apiResponseData = $shipment->api_response_data ?? [];
|
||||
$apiResponseData['cancellation_error'] = [
|
||||
'status' => 'failed',
|
||||
'reason' => $reason,
|
||||
'http_status' => $exception ? $this->extractHttpStatus($exception->getMessage()) : null,
|
||||
'dhl_code' => $exception ? $this->extractDhlErrorCode($exception->getMessage()) : null,
|
||||
'detail' => $detail,
|
||||
'exception_class' => $exception ? $exception::class : null,
|
||||
'occurred_at' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$shipment->update(['api_response_data' => $apiResponseData]);
|
||||
}
|
||||
|
||||
private function extractHttpStatus(string $message): ?int
|
||||
{
|
||||
if (preg_match('/\b([45][0-9]{2})\b/', $message, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractDhlErrorCode(string $message): ?string
|
||||
{
|
||||
if (preg_match('/\b([A-Z]{2}-[A-Za-z0-9_-]+)\b/', $message, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
89
app/Services/DhlShipmentWeightCalculator.php
Normal file
89
app/Services/DhlShipmentWeightCalculator.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ShoppingOrder;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DhlShipmentWeightCalculator
|
||||
{
|
||||
public const DEFAULT_WEIGHT_KG = 1.0;
|
||||
|
||||
public const DEFAULT_MAX_WEIGHT_KG = 31.5;
|
||||
|
||||
public const PRODUCT_MAX_WEIGHTS_KG = [
|
||||
'V01PAK' => 31.5,
|
||||
'V53PAK' => 31.5,
|
||||
'V62KP' => 1.0,
|
||||
];
|
||||
|
||||
public function calculate(ShoppingOrder $order): float
|
||||
{
|
||||
$this->loadOrderItems($order);
|
||||
|
||||
$baseWeightGrams = max((int) ($order->weight ?? 0), 0);
|
||||
$compensationWeightGrams = $this->calculateCompensationWeightGrams($order);
|
||||
$totalWeightGrams = $baseWeightGrams + $compensationWeightGrams;
|
||||
|
||||
if ($totalWeightGrams <= 0) {
|
||||
return self::DEFAULT_WEIGHT_KG;
|
||||
}
|
||||
|
||||
return $this->roundWeightKg($totalWeightGrams / 1000);
|
||||
}
|
||||
|
||||
public function getMaxWeightKgForProduct(?string $productCode): float
|
||||
{
|
||||
$productCode = strtoupper(trim((string) $productCode));
|
||||
|
||||
return self::PRODUCT_MAX_WEIGHTS_KG[$productCode] ?? self::DEFAULT_MAX_WEIGHT_KG;
|
||||
}
|
||||
|
||||
public function assertWithinProductLimit(float $weightKg, ?string $productCode): void
|
||||
{
|
||||
$maxWeightKg = $this->getMaxWeightKgForProduct($productCode);
|
||||
|
||||
if ($weightKg > $maxWeightKg) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Gewicht %.3f kg ueberschreitet das DHL-Maximalgewicht fuer %s (%.1f kg).',
|
||||
$weightKg,
|
||||
$productCode ?: 'das gewaehlte Produkt',
|
||||
$maxWeightKg
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateCompensationWeightGrams(ShoppingOrder $order): int
|
||||
{
|
||||
$items = $order->shopping_order_items ?? collect();
|
||||
$weightGrams = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ((int) ($item->comp ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$productWeight = (int) ($item->product?->weight ?? 0);
|
||||
if ($productWeight <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quantity = max((int) ($item->qty ?? 1), 1);
|
||||
$weightGrams += $productWeight * $quantity;
|
||||
}
|
||||
|
||||
return $weightGrams;
|
||||
}
|
||||
|
||||
private function roundWeightKg(float $weightKg): float
|
||||
{
|
||||
return round(max($weightKg, 0.1), 3);
|
||||
}
|
||||
|
||||
private function loadOrderItems(ShoppingOrder $order): void
|
||||
{
|
||||
if ($order->exists && ! $order->relationLoaded('shopping_order_items')) {
|
||||
$order->loadMissing('shopping_order_items.product');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -307,6 +307,16 @@ class DhlTrackingService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tracking immediately, bypassing queue dispatch.
|
||||
*/
|
||||
public function updateTrackingNow(DhlShipment $shipment, array $options = []): array
|
||||
{
|
||||
$settingController = new SettingController;
|
||||
|
||||
return $this->updateTrackingSync($shipment, $options, $settingController->getDhlConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tracking asynchronously using queue
|
||||
*/
|
||||
|
|
@ -366,7 +376,7 @@ class DhlTrackingService
|
|||
$result = $this->trackShipment($shipment->dhl_shipment_no);
|
||||
|
||||
if ($result['success']) {
|
||||
$internalStatus = $this->mapDhlStatusToInternal($result['status']);
|
||||
$internalStatus = self::mapDhlStatusToInternal($result['status']);
|
||||
|
||||
// Update shipment with tracking data
|
||||
$updateData = [
|
||||
|
|
@ -481,7 +491,7 @@ class DhlTrackingService
|
|||
// Remove from map so we can detect missing ones later
|
||||
unset($shipmentMap[$trackingNo]);
|
||||
|
||||
$internalStatus = $this->mapDhlStatusToInternal($trackingResult['status']);
|
||||
$internalStatus = self::mapDhlStatusToInternal($trackingResult['status']);
|
||||
|
||||
$updateData = [
|
||||
'status' => $internalStatus,
|
||||
|
|
@ -593,19 +603,25 @@ class DhlTrackingService
|
|||
/**
|
||||
* Map DHL status codes to internal status
|
||||
*/
|
||||
private function mapDhlStatusToInternal(string $dhlStatus): string
|
||||
public static function mapDhlStatusToInternal(string $dhlStatus): string
|
||||
{
|
||||
$statusMap = [
|
||||
'pre-transit' => 'created',
|
||||
'pre_transit' => 'created',
|
||||
'pretransit' => 'created',
|
||||
'transit' => 'in_transit',
|
||||
'in-transit' => 'in_transit',
|
||||
'in_transit' => 'in_transit',
|
||||
'out-for-delivery' => 'out_for_delivery',
|
||||
'out_for_delivery' => 'out_for_delivery',
|
||||
'delivered' => 'delivered',
|
||||
'failure' => 'failed',
|
||||
'failed' => 'failed',
|
||||
'returned' => 'returned',
|
||||
'exception' => 'exception',
|
||||
];
|
||||
|
||||
return $statusMap[$dhlStatus] ?? 'unknown';
|
||||
return $statusMap[strtolower($dhlStatus)] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue