mivita/app/Services/DhlModalService.php
2025-10-20 17:42:08 +02:00

497 lines
17 KiB
PHP

<?php
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Models\Country;
use Illuminate\Support\Facades\Log;
use Exception;
/**
* 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(),
'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);
// 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
* @return ShoppingOrder|null
*/
private function loadOrder($id): ?ShoppingOrder
{
return ShoppingOrder::with([
'shopping_order_items',
'shopping_user',
'dhlShipments' // Include DHL shipments
])->find($id);
}
/**
* Get existing DHL shipments for the order
*
* @param ShoppingOrder $order
* @return array
*/
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()
];
})->toArray();
}
/**
* Calculate order weight in kg
*
* @param ShoppingOrder $order
* @return float
*/
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);
*/
}
/**
* Process and parse shipping address from order
*
* @param ShoppingOrder $order
* @return array
*/
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' => '',
];
// Parse and separate street name and number
$this->parseStreetAddress($addressData);
return $addressData;
}
/**
* Parse street address and separate street name from house number
*
* @param array &$addressData
*/
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
*
* @param array $address
* @return array Validation result with 'valid', 'errors', and 'warnings' keys
*/
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.';
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
];
}
/**
* Get empty address template
*
* @return array
*/
private function getEmptyAddress(): array
{
return [
'firstname' => '',
'lastname' => '',
'company' => '',
'address' => '',
'address_2' => '',
'zipcode' => '',
'city' => '',
'country' => null,
'phone' => '',
'email' => '',
];
}
/**
* 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
*
* @return array
*/
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['V62WP'])) {
$productCodes['V62WP'] = 'DHL Warenpost National';
}
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',
'V62WP' => 'DHL Warenpost National'
];
}
return $productCodes;
}
/**
* Validate shipment parameters before API call
*
* @param array $shipmentData
* @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.';
}
// 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.";
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings
];
}
/**
* Prepare address data for DHL API
*
* @param array $formData
* @return array
*/
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
];
}
}