mivita/app/Services/DhlShipmentService.php

426 lines
16 KiB
PHP

<?php
namespace App\Services;
use Acme\Dhl\Exceptions\DhlAddressValidationException;
use Acme\Dhl\Models\DhlShipment;
use App\Http\Controllers\SettingController;
use App\Jobs\CancelShipmentJob;
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
*/
class DhlShipmentService
{
/**
* Create a DHL shipment (sync or async based on config)
*/
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;
$dhlConfig = $settingController->getDhlConfig();
Log::info('[DHL Service] Loaded DHL configuration', self::sanitizeDhlConfigForLog($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);
} else {
return $this->createShipmentSync($order, $weight, $options, $dhlConfig);
}
}
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
*/
private function createShipmentAsync(ShoppingOrder $order, float $weight, array $options, array $dhlConfig): array
{
try {
// Dispatch job with pre-loaded config
CreateShipmentJob::dispatch($order, $weight, $options, $dhlConfig);
Log::info('[DHL Service] Shipment creation dispatched to queue', [
'order_id' => $order->id,
'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,
];
} catch (Exception $e) {
Log::error('[DHL Service] Failed to dispatch shipment creation', [
'error' => $e->getMessage(),
'order_id' => $order->id,
]);
return [
'success' => false,
'message' => 'Fehler beim Einreihen der Sendungserstellung: '.$e->getMessage(),
'queued' => false,
];
}
}
/**
* Create shipment synchronously
*/
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,
]);
// Create DHL client directly with correct base URL
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
// Prepare order data using helper
$orderData = DhlDataHelper::prepareOrderData($order, $weight, $options, $dhlConfig);
Log::info('[DHL Service] Order data prepared for DHL API', self::sanitizeOrderDataForLog($orderData));
// Create the shipment directly
$result = $shippingService->createLabel($orderData);
Log::info('[DHL Service] Shipment created successfully (sync)', [
'order_id' => $order->id,
'shipment_number' => $result['shipmentNumber'] ?? 'N/A',
'label_path' => $result['labelPath'] ?? 'N/A',
]);
return [
'success' => true,
'message' => 'Versandlabel erfolgreich erstellt!',
'queued' => false,
'order_id' => $order->id,
'shipment_number' => $result['shipmentNumber'] ?? null,
'tracking_number' => $result['trackingNumber'] ?? null,
'label_path' => $result['labelPath'] ?? null,
'label_url' => $result['labelUrl'] ?? null,
];
} catch (DhlAddressValidationException $e) {
Log::warning('[DHL Service] Shipment address validation failed (sync)', [
'order_id' => $order->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'type' => 'dhl_address_validation',
'message' => $e->getMessage(),
'errors' => [$e->getMessage()],
'queued' => false,
'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)
*/
public function cancelShipment(DhlShipment $shipment, array $options = []): array
{
// Get DHL configuration
$settingController = new SettingController;
$dhlConfig = $settingController->getDhlConfig();
// Check if queue should be used
$useQueue = $dhlConfig['use_queue'] ?? false;
if ($useQueue) {
return $this->cancelShipmentAsync($shipment, $options, $dhlConfig);
} else {
return $this->cancelShipmentSync($shipment, $options, $dhlConfig);
}
}
/**
* Cancel shipment asynchronously using queue
*/
private function cancelShipmentAsync(DhlShipment $shipment, array $options, array $dhlConfig): array
{
try {
// Dispatch job
CancelShipmentJob::dispatch($shipment, $options);
Log::info('[DHL Service] Shipment cancellation dispatched to queue', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
]);
return [
'success' => true,
'message' => 'Sendung wird storniert...',
'queued' => true,
'shipment_id' => $shipment->id,
];
} catch (Exception $e) {
Log::error('[DHL Service] Failed to dispatch shipment cancellation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return [
'success' => false,
'message' => 'Fehler beim Einreihen der Stornierung: '.$e->getMessage(),
'queued' => false,
];
}
}
/**
* Cancel shipment synchronously
*/
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,
];
}
// 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->getStatusTranslation().'" nicht storniert werden. Nur Status "Erstellt" oder "Wartend" sind stornierbar.',
'queued' => false,
'shipment_id' => $shipment->id,
];
}
Log::info('[DHL Service] Cancelling shipment synchronously', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'base_url' => $dhlConfig['base_url'],
]);
// Create DHL client
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
// Cancel the shipment directly
$success = $shippingService->cancelLabel($shipment->dhl_shipment_no);
if ($success) {
Log::info('[DHL Service] Shipment cancelled successfully (sync)', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
]);
return [
'success' => true,
'message' => 'Sendung wurde erfolgreich storniert!',
'queued' => false,
'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(),
]);
return [
'success' => false,
'message' => $e->getMessage(),
'queued' => false,
'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(),
]);
// Check if it's an API authentication/resource error
$errorMessage = $e->getMessage();
if (strpos($errorMessage, 'RF-UndefinedResource') !== false) {
return [
'success' => false,
'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,
];
}
return [
'success' => false,
'message' => 'Fehler beim Stornieren der Sendung: '.$errorMessage,
'queued' => false,
'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;
}
/**
* Build a redacted view of the DHL configuration for safe logging.
*
* Never include `api_key`, `username`, `password`, `api_secret` or the
* full billing number itself. Only return boolean presence flags and
* non-sensitive metadata.
*
* @param array<string, mixed> $dhlConfig
* @return array<string, mixed>
*/
public static function sanitizeDhlConfigForLog(array $dhlConfig): array
{
return [
'base_url' => $dhlConfig['base_url'] ?? null,
'sandbox' => $dhlConfig['sandbox'] ?? null,
'test_mode' => $dhlConfig['test_mode'] ?? null,
'has_api_key' => ! empty($dhlConfig['api_key']),
'has_username' => ! empty($dhlConfig['username']),
'has_password' => ! empty($dhlConfig['password']),
'has_api_secret' => ! empty($dhlConfig['api_secret']),
'use_queue' => (bool) ($dhlConfig['use_queue'] ?? false),
'default_product' => $dhlConfig['default_product'] ?? null,
'label_format' => $dhlConfig['label_format'] ?? null,
'print_format' => $dhlConfig['print_format'] ?? null,
'print_only_if_codeable' => (bool) ($dhlConfig['print_only_if_codeable'] ?? false),
'international_countries' => $dhlConfig['international_countries'] ?? [],
'account_numbers_configured' => array_keys(array_filter($dhlConfig['account_numbers'] ?? [])),
];
}
/**
* Build a redacted view of the order data prepared for DHL.
*
* Strips personally identifiable information like full names, addresses,
* phone numbers and e-mail addresses. Keeps the routing-relevant fields
* needed to debug a failing label generation.
*
* @param array<string, mixed> $orderData
* @return array<string, mixed>
*/
public static function sanitizeOrderDataForLog(array $orderData): array
{
$consigneeCountry = $orderData['consignee']['country'] ?? null;
$consigneePostal = $orderData['consignee']['postalCode'] ?? null;
return [
'order_id' => $orderData['order_id'] ?? null,
'product_code' => $orderData['product_code'] ?? null,
'weight_kg' => $orderData['weight_kg'] ?? null,
'label_format' => $orderData['label_format'] ?? null,
'print_format' => $orderData['print_format'] ?? null,
'print_only_if_codeable' => (bool) ($orderData['print_only_if_codeable'] ?? false),
'consignee_country' => $consigneeCountry,
'consignee_postal_prefix' => is_string($consigneePostal) && $consigneePostal !== ''
? mb_substr($consigneePostal, 0, 2)
: null,
'consignee_has_post_number' => ! empty($orderData['consignee']['postNumber']),
'has_reference' => ! empty($orderData['reference']),
];
}
}