444 lines
16 KiB
PHP
444 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Country;
|
|
use App\Models\ShoppingOrder;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* DHL Modal Service
|
|
*
|
|
* Service class that handles all business logic for the DHL shipment creation modal.
|
|
* Validates order data, processes addresses, and prepares data for the view.
|
|
*/
|
|
class DhlModalService
|
|
{
|
|
/**
|
|
* @var array DHL configuration
|
|
*/
|
|
private $config;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->config = config('dhl');
|
|
}
|
|
|
|
/**
|
|
* Prepare modal data for DHL shipment creation
|
|
*
|
|
* @param mixed $id Order ID or 'new'
|
|
* @param array $data Additional data from the request
|
|
* @return array Prepared data for the view
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function prepareModalData($id, array $data): array
|
|
{
|
|
$result = [
|
|
'order' => null,
|
|
'orderWeight' => 1.0,
|
|
'shippingAddress' => null,
|
|
'availableCountries' => $this->getAvailableCountries(),
|
|
'productCodes' => $this->getAvailableProductCodes(),
|
|
'productSuggestions' => (new DhlProductResolver)->getProductSuggestionsByCountry(),
|
|
'selectedProductCode' => null,
|
|
'errors' => [],
|
|
'warnings' => [],
|
|
'existingShipments' => [],
|
|
'modalMode' => 'search', // 'search', 'create', 'info'
|
|
];
|
|
|
|
// If no order ID or 'new', return empty data for order selection
|
|
if (! $id || $id === 'new') {
|
|
return $result;
|
|
}
|
|
|
|
try {
|
|
// Load and validate order
|
|
$order = $this->loadOrder($id);
|
|
if (! $order) {
|
|
$result['errors'][] = "Bestellung #{$id} wurde nicht gefunden.";
|
|
|
|
return $result;
|
|
}
|
|
|
|
$result['order'] = $order;
|
|
|
|
// Check for existing DHL shipments
|
|
$existingShipments = $this->getExistingShipments($order);
|
|
$result['existingShipments'] = $existingShipments;
|
|
|
|
// Check if force_create is requested
|
|
$forceCreate = isset($data['force_create']) && $data['force_create'];
|
|
|
|
// Determine modal mode based on existing shipments and force_create
|
|
if (! empty($existingShipments) && ! $forceCreate) {
|
|
$result['modalMode'] = 'info';
|
|
Log::info('[DHL Modal] Order has existing shipments, showing info mode', [
|
|
'order_id' => $order->id,
|
|
'shipment_count' => count($existingShipments),
|
|
]);
|
|
} else {
|
|
$result['modalMode'] = 'create';
|
|
|
|
// Calculate order weight
|
|
$result['orderWeight'] = $this->calculateOrderWeight($order);
|
|
|
|
// Process and validate shipping address
|
|
$result['shippingAddress'] = $this->processShippingAddress($order);
|
|
$result['selectedProductCode'] = $this->getSuggestedProductCode($result['shippingAddress']);
|
|
|
|
// Validate address completeness
|
|
$addressValidation = $this->validateAddress($result['shippingAddress']);
|
|
if (! $addressValidation['valid']) {
|
|
$result['errors'] = array_merge($result['errors'], $addressValidation['errors']);
|
|
}
|
|
if (! empty($addressValidation['warnings'])) {
|
|
$result['warnings'] = array_merge($result['warnings'], $addressValidation['warnings']);
|
|
}
|
|
|
|
Log::info('[DHL Modal] Prepared modal data for creation', [
|
|
'order_id' => $order->id,
|
|
'weight' => $result['orderWeight'],
|
|
'address_valid' => empty($result['errors']),
|
|
]);
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::error('[DHL Modal] Error preparing modal data', [
|
|
'order_id' => $id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
$result['errors'][] = 'Fehler beim Laden der Bestelldaten: '.$e->getMessage();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Load order with required relationships
|
|
*
|
|
* @param mixed $id
|
|
*/
|
|
private function loadOrder($id): ?ShoppingOrder
|
|
{
|
|
return ShoppingOrder::with([
|
|
'shopping_order_items.product',
|
|
'shopping_user',
|
|
'dhlShipments', // Include DHL shipments
|
|
])->find($id);
|
|
}
|
|
|
|
/**
|
|
* Get existing DHL shipments for the order
|
|
*/
|
|
private function getExistingShipments(ShoppingOrder $order): array
|
|
{
|
|
$shipments = $order->dhlShipments()
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
|
|
return $shipments->map(function ($shipment) {
|
|
return [
|
|
'id' => $shipment->id,
|
|
'shipment_number' => $shipment->dhl_shipment_no,
|
|
'tracking_number' => $shipment->routing_code,
|
|
'type' => $shipment->type,
|
|
'status' => $shipment->status,
|
|
'status_translated' => $shipment->getStatusTranslation(),
|
|
'type_translated' => $shipment->getTypeTranslation(),
|
|
'product_code_translated' => $shipment->getProductCodeTranslation(),
|
|
'weight' => $shipment->weight_kg,
|
|
'product_code' => $shipment->product_code,
|
|
'label_path' => $shipment->label_path,
|
|
'created_at' => $shipment->created_at->toDateTimeString(),
|
|
'tracking_status' => $shipment->tracking_status,
|
|
'tracking_status_translated' => $shipment->tracking_status ? \Acme\Dhl\Models\DhlShipment::getStatusTranslationFor($shipment->tracking_status) : null,
|
|
'last_tracked_at' => $shipment->last_tracked_at,
|
|
'can_cancel' => $shipment->canCancel(),
|
|
'is_delivered' => $shipment->isDelivered(),
|
|
'email' => $shipment->email, // E-Mail für Tracking-E-Mail Button
|
|
'can_send_email' => $shipment->canSendTrackingEmail(),
|
|
];
|
|
})->toArray();
|
|
}
|
|
|
|
/**
|
|
* Calculate order weight in kg
|
|
*/
|
|
private function calculateOrderWeight(ShoppingOrder $order): float
|
|
{
|
|
return (new DhlShipmentWeightCalculator)->calculate($order);
|
|
}
|
|
|
|
/**
|
|
* Process and parse shipping address from order
|
|
*/
|
|
private function processShippingAddress(ShoppingOrder $order): array
|
|
{
|
|
$shoppingUser = $order->shopping_user;
|
|
|
|
if (! $shoppingUser) {
|
|
return $this->getEmptyAddress();
|
|
}
|
|
|
|
// Determine if shipping address is different from billing
|
|
$useShipping = ! ($shoppingUser->same_as_billing ?? true);
|
|
|
|
// Extract address data
|
|
$addressData = [
|
|
'firstname' => $useShipping ? ($shoppingUser->shipping_firstname ?? '') : ($shoppingUser->billing_firstname ?? ''),
|
|
'lastname' => $useShipping ? ($shoppingUser->shipping_lastname ?? '') : ($shoppingUser->billing_lastname ?? ''),
|
|
'company' => $useShipping ? ($shoppingUser->shipping_company ?? '') : ($shoppingUser->billing_company ?? ''),
|
|
'address' => $useShipping ? ($shoppingUser->shipping_address ?? '') : ($shoppingUser->billing_address ?? ''),
|
|
'address_2' => $useShipping ? ($shoppingUser->shipping_address_2 ?? '') : ($shoppingUser->billing_address_2 ?? ''),
|
|
'zipcode' => $useShipping ? ($shoppingUser->shipping_zipcode ?? '') : ($shoppingUser->billing_zipcode ?? ''),
|
|
'city' => $useShipping ? ($shoppingUser->shipping_city ?? '') : ($shoppingUser->billing_city ?? ''),
|
|
'country' => $useShipping ? ($shoppingUser->shipping_country ?? null) : ($shoppingUser->billing_country ?? null),
|
|
'phone' => $useShipping ? ($shoppingUser->shipping_phone ?? '') : ($shoppingUser->billing_phone ?? ''),
|
|
'email' => $shoppingUser->billing_email ?? '',
|
|
'houseNumber' => '',
|
|
// DHL Postnummer für Packstation/Paketbox (nur bei Versandadresse)
|
|
'postnumber' => $useShipping ? ($shoppingUser->shipping_postnumber ?? '') : '',
|
|
];
|
|
|
|
// Parse and separate street name and number
|
|
$this->parseStreetAddress($addressData);
|
|
|
|
return $addressData;
|
|
}
|
|
|
|
/**
|
|
* Parse street address and separate street name from house number
|
|
*/
|
|
private function parseStreetAddress(array &$addressData): void
|
|
{
|
|
$address = trim($addressData['address']);
|
|
// If address_2 is empty and address contains both street and number
|
|
if (! empty($address)) {
|
|
// Try to separate street name and house number
|
|
$patterns = [
|
|
// Pattern 1: "Musterstraße 123" or "Musterstraße 123a"
|
|
'/^(.+?)\s+(\d+[a-zA-Z]?)$/u',
|
|
// Pattern 2: "Musterstraße 123-125" or "Musterstraße 123/125"
|
|
'/^(.+?)\s+(\d+[-\/]\d+[a-zA-Z]?)$/u',
|
|
// Pattern 3: "123 Musterstraße" (number first)
|
|
'/^(\d+[a-zA-Z]?)\s+(.+)$/u',
|
|
];
|
|
|
|
foreach ($patterns as $index => $pattern) {
|
|
if (preg_match($pattern, $address, $matches)) {
|
|
if ($index === 2) {
|
|
// Number first pattern
|
|
$addressData['address'] = trim($matches[2]);
|
|
$addressData['houseNumber'] = trim($matches[1]);
|
|
} else {
|
|
// Street first patterns
|
|
$addressData['address'] = trim($matches[1]);
|
|
$addressData['houseNumber'] = trim($matches[2]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up the address data
|
|
$addressData['address'] = trim($addressData['address']);
|
|
$addressData['houseNumber'] = trim($addressData['houseNumber']);
|
|
}
|
|
|
|
/**
|
|
* Validate address completeness and format
|
|
*
|
|
* @return array Validation result with 'valid', 'errors', and 'warnings' keys
|
|
*/
|
|
private function validateAddress(array $address): array
|
|
{
|
|
$result = (new DhlAddressValidator)->validate($address);
|
|
|
|
return [
|
|
'valid' => $result['can_create_label'],
|
|
'errors' => $result['errors'],
|
|
'warnings' => $result['warnings'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get empty address template
|
|
*/
|
|
private function getEmptyAddress(): array
|
|
{
|
|
return [
|
|
'firstname' => '',
|
|
'lastname' => '',
|
|
'company' => '',
|
|
'address' => '',
|
|
'address_2' => '',
|
|
'zipcode' => '',
|
|
'city' => '',
|
|
'country' => null,
|
|
'phone' => '',
|
|
'email' => '',
|
|
'postnumber' => '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get available countries for shipping
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Collection
|
|
*/
|
|
private function getAvailableCountries()
|
|
{
|
|
return Country::where('active', 1)->get();
|
|
}
|
|
|
|
/**
|
|
* Get available DHL product codes from settings
|
|
*/
|
|
private function getAvailableProductCodes(): array
|
|
{
|
|
// Get DHL configuration with merged settings
|
|
$settingController = new \App\Http\Controllers\SettingController;
|
|
$dhlConfig = $settingController->getDhlConfig();
|
|
|
|
$productCodes = [];
|
|
|
|
// Add products based on configured account numbers
|
|
$accountNumbers = $dhlConfig['account_numbers'] ?? [];
|
|
|
|
if (! empty($accountNumbers['V01PAK'])) {
|
|
$productCodes['V01PAK'] = 'DHL Paket National';
|
|
}
|
|
|
|
if (! empty($accountNumbers['V53PAK'])) {
|
|
$productCodes['V53PAK'] = 'DHL Paket International';
|
|
}
|
|
|
|
if (! empty($accountNumbers['V62KP'])) {
|
|
$productCodes['V62KP'] = 'DHL Kleinpaket';
|
|
}
|
|
|
|
if (! empty($accountNumbers['V07PAK'])) {
|
|
$productCodes['V07PAK'] = 'DHL Retoure Online';
|
|
}
|
|
|
|
// Fallback to default if no account numbers configured
|
|
if (empty($productCodes)) {
|
|
$productCodes = [
|
|
'V01PAK' => 'DHL Paket National',
|
|
'V53PAK' => 'DHL Paket International',
|
|
'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
|
|
*
|
|
* @return array Validation result
|
|
*/
|
|
public function validateShipmentData(array $shipmentData): array
|
|
{
|
|
$errors = [];
|
|
$warnings = [];
|
|
|
|
// Weight validation
|
|
$weight = floatval($shipmentData['weight'] ?? 0);
|
|
if ($weight < 0.1) {
|
|
$errors[] = 'Gewicht muss mindestens 0.1 kg betragen.';
|
|
} elseif ($weight > 31.5) {
|
|
$errors[] = 'Gewicht darf maximal 31.5 kg betragen.';
|
|
}
|
|
|
|
// Product code validation
|
|
$productCode = $shipmentData['product_code'] ?? '';
|
|
$availableProducts = array_keys($this->getAvailableProductCodes());
|
|
if (! in_array($productCode, $availableProducts)) {
|
|
$errors[] = 'Ungültiger Produktcode ausgewählt.';
|
|
}
|
|
|
|
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,
|
|
'warnings' => $warnings,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Prepare address data for DHL API
|
|
*/
|
|
public function prepareAddressForApi(array $formData): array
|
|
{
|
|
$country = null;
|
|
if (! empty($formData['shipping_country_id'])) {
|
|
$country = Country::find($formData['shipping_country_id']);
|
|
}
|
|
|
|
return [
|
|
'firstname' => trim($formData['shipping_firstname'] ?? ''),
|
|
'lastname' => trim($formData['shipping_lastname'] ?? ''),
|
|
'company' => trim($formData['shipping_company'] ?? ''),
|
|
'address' => trim($formData['shipping_address'] ?? ''),
|
|
'address_2' => trim($formData['shipping_address_2'] ?? ''),
|
|
'houseNumber' => trim($formData['shipping_houseNumber'] ?? ''),
|
|
'zipcode' => trim($formData['shipping_zipcode'] ?? ''),
|
|
'city' => trim($formData['shipping_city'] ?? ''),
|
|
'country_id' => $country?->id,
|
|
'country' => $country, // Store country object for DhlDataHelper
|
|
'phone' => trim($formData['shipping_phone'] ?? ''),
|
|
'email' => trim($formData['shipping_email'] ?? ''), // Add email if available
|
|
'postnumber' => trim($formData['shipping_postnumber'] ?? ''), // DHL Postnummer für Packstation/Paketbox
|
|
];
|
|
}
|
|
}
|