DHL Modul v0.5 Shipping Label ok

This commit is contained in:
Kevin Adametz 2025-08-22 18:18:26 +02:00
parent 480fdc65ed
commit 8fdaa0ba1d
122 changed files with 17938 additions and 2239 deletions

62
.env
View file

@ -3,6 +3,7 @@ APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:HrWQ9AV3Zt2TU0iq1OeUUpTUaXwNUdh8xHmx7RXTif4=
APP_URL=https://mivita.test/
APP_URL_CRM=https://my.mivita.test
APP_DOMAIN=mivita
APP_TLD_CARE=.test
APP_TLD_SHOP=.lshop
@ -57,7 +58,7 @@ BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_LIFETIME=120
QUEUE_DRIVER=sync
QUEUE_DRIVER=database
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
@ -86,3 +87,62 @@ MIVITA_ABO_BOOKING_DAYS=7
MIVITA_REMIND_LAST_DAYS=2
MIVITA_EDIT_DATA_PASS=mivita
MIVITA_ADD_NUMBER_ID=946
# =============================================================================
# =============================================================================
# DHL VERSANDMODUL KONFIGURATION (acme/laravel-dhl)
# =============================================================================
# DHL API Zugangsdaten (konsolidiert)
DHL_BASE_URL=https://api-eu.dhl.com
DHL_API_KEY=AxGBdF8DBdIAmuhqvG0ASBRKFvyV7ypX
DHL_USERNAME=riwa-tec
DHL_PASSWORD=MivitaCare!!2025
DHL_BILLING_NUMBER=63144073550101
# DHL Standard-Einstellungen
DHL_PRODUCT=V01PAK
DHL_LABEL_FORMAT=PDF
DHL_PRINT_FORMAT=910-300-710
DHL_RETOURE_PRINT_FORMAT=910-300-700
DHL_PROFILE=STANDARD_GRUPPENPROFIL
# DHL Queue Configuration
# Set to true for background processing (requires queue worker)
# Set to false for immediate processing
DHL_USE_QUEUE=false
# DHL Account Numbers (für verschiedene Produkte)
DHL_ACCOUNT_NUMBER_DEFAULT=63144073550101
DHL_ACCOUNT_NUMBER_V01PAK=63144073550101 # DHL Paket National
DHL_ACCOUNT_NUMBER_V62WP=63144073556201 # Warenpost National
DHL_ACCOUNT_NUMBER_V53PAK=63144073555301 # DHL Paket International
DHL_ACCOUNT_NUMBER_V07PAK=63144073550701 # DHL Retoure Online
#sandbox
DHL_ACCOUNT_NUMBER_DEFAULT=33333333330101
DHL_ACCOUNT_NUMBER_V01PAK=33333333330102 # DHL Paket National
DHL_ACCOUNT_NUMBER_V62WP=33333333336601 # Warenpost National
DHL_ACCOUNT_NUMBER_V53PAK=33333333335301 # DHL Paket International
DHL_ACCOUNT_NUMBER_V07PAK=33333333330702 # DHL Retoure Online
#V66WPI|33333333336601
#VO1PAK 33333333330102 33333333330101
#V53WPAK 33333333335301
#VO7PAK 33333333330702
# DHL Absenderadresse
DHL_SENDER_COMPANY="mivita care gmbh"
DHL_SENDER_NAME=""
DHL_SENDER_STREET="Leinfeld"
DHL_SENDER_STREET_NUMBER=2
DHL_SENDER_POSTAL_CODE=87755
DHL_SENDER_CITY=Kirchhaslach
DHL_SENDER_COUNTRY=DE
DHL_SENDER_EMAIL=versand@mivita.care
DHL_SENDER_PHONE="+49 123 456789"
# DHL Legacy/Compatibility Settings
DHL_API_TYPE=developer
DHL_API_SECRET=OyoeePEbYmY1EuOG
DHL_SANDBOX=true
DHL_TEST_MODE=true

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,213 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use App\User;
use App\Services\Util;
use App\Models\UserHistory;
use App\Models\UserMessage;
use App\Mail\MailCustomMessage;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class CheckPaymentsAccount extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'payments:check-accounts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Überprüft Benutzer-Zahlungskonten und sendet Erinnerungen basierend auf Erneuerungsdaten.';
/**
* Execute the console command.
*/
public function handle()
{
Log::channel('cron')->info('COMMAND [payments:check-accounts] started.');
$this->info('COMMAND [payments:check-accounts] started.');
// Die Logik wurde 1:1 aus der checkPaymentsAccounts-Methode übernommen
$renewalDate = Carbon::now()->modify('+'.(config('mivita.remind_first_days')+1).' days');
Log::channel('cron')->info('Erneuerungsdatum für Zahlungen: ' . $renewalDate->format('Y-m-d H:i:s'));
$users = User::where('payment_account', '!=', NULL)
->where('active', '=', 1)
->where('blocked', '!=', 1)
->where('payment_account', '<', $renewalDate)
->get();
Log::channel('cron')->info('Found ' . $users->count() . ' users for payment reminders.');
$this->info('Found ' . $users->count() . ' users for payment reminders.');
foreach ($users as $user){
Log::channel('cron')->info('Prüfe Zahlungserinnerungen für Benutzer: ' . $user->email);
$this->checkReminderPayments($user);
}
Log::channel('cron')->info('COMMAND [payments:check-accounts] finished.');
$this->info('COMMAND [payments:check-accounts] finished.');
return 0; // Success
}
/**
* Überprüft und sendet Zahlungserinnerungen basierend auf Benutzerkontostand
*
* RULES:
* > 21 remind_first_days = 31 reminder_first
* > 21 remind_first_days + sepa = 32 reminder_first_sepa
* > 14 remind_sec_days = 33 reminder_sec
* > 2 remind_last_days = 34 reminder_last
* > 0 deaktiv = 35 reminder_deaktiv
* > 0 deaktiv + sepa = 36 reminder_deaktiv_sepa
* == 7 abo_booking_days + sepa + cron = 37 reminder_collect_sepa
*
* @param User $user Benutzer
* @return void
*/
private function checkReminderPayments(User $user)
{
//35 reminder_deaktiv, 36 reminder_deaktiv_sepa
if(!$user->isActiveAccount()){
Log::channel('cron')->info('Inaktives Konto für Benutzer: ' . $user->email);
$this->checkIsReminderSend($user, 35);
return;
}
//34 reminder_last
if($user->daysActiveAccount() <= config('mivita.remind_last_days')){
Log::channel('cron')->info('Letzte Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$this->checkIsReminderSend($user, 34);
return;
}
//33 reminder_sec
if($user->daysActiveAccount() <= config('mivita.remind_sec_days')){
Log::channel('cron')->info('Zweite Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$this->checkIsReminderSend($user, 33);
return;
}
//31 reminder_first
if($user->daysActiveAccount() > config('mivita.remind_sec_days')){
Log::channel('cron')->info('Erste Erinnerung für Benutzer: ' . $user->email . ' (Tage aktiv: ' . $user->daysActiveAccount() . ')');
$this->checkIsReminderSend($user, 31);
return;
}
}
/**
* Überprüft, ob eine Erinnerung bereits gesendet wurde
*
* @param User $user Benutzer
* @param int $status Status-Code der Erinnerung
* @return bool
*/
private function checkIsReminderSend(User $user, $status)
{
$isSend = UserHistory::whereUserId($user->id)
->whereAction('reminder_payments')
->whereIdentifier($user->payment_account)
->whereStatus($status)
->latest()
->first();
if($isSend){
Log::channel('cron')->info('Erinnerung bereits gesendet für Benutzer: ' . $user->email . ' (Status: ' . $status . ')');
return true;
}
Log::channel('cron')->info('Sende neue Erinnerung für Benutzer: ' . $user->email . ' (Status: ' . $status . ')');
$referenz = $this->sendReminderMail($user, $status);
UserHistory::create([
'user_id' => $user->id,
'action' => 'reminder_payments',
'referenz' => $referenz,
'identifier' => $user->payment_account,
'status' => $status
]);
return false;
}
/**
* Sendet eine Erinnerungs-E-Mail an den Benutzer
*
* @param User $user Benutzer
* @param int $status Status-Code der Erinnerung
* @return int
*/
private function sendReminderMail(User $user, $status)
{
$days = abs($user->daysActiveAccount());
$pay_date = Carbon::parse($user->payment_account)->modify('- ' . config('mivita.abo_booking_days') . ' days')->format('d.m.Y');
$datetime = $user->getPaymentAccountDateFormat();
$price = "";
if($user->payment_order_id && isset($user->payment_order_product->price)){
$price = 'von ' . $user->payment_order_product->getFormattedPrice() . ' EUR';
}
$message = __('reminder.copy_first_' . $status, ['days' => $days, 'datetime' => $datetime, 'price' => $price, 'pay_date' => $pay_date]);
$message_last = __('reminder.copy_last_' . $status, ['days' => $days, 'datetime' => $datetime, 'price' => $price, 'pay_date' => $pay_date]);
$button = __('reminder.button_' . $status);
$message = preg_replace("/[\n\r]/", "", $message);
$message_last = preg_replace("/[\n\r]/", "", $message_last);
$data = [
'subject' => __('reminder.subject') . " | ID: " . $status,
'message' => $message,
'message_last' => $message_last,
'url' => route('user_membership'),
'button' => $button,
];
$sender = User::find(1);
$customer_mail = UserMessage::create(['user_id' => $user->id, 'send_user_id' => $sender->id, 'email' => $user->email, 'subject' => $data['subject'], 'message' => $data['message'] . " " . $data['message_last']]);
try {
if(!Util::isTestSystem()){
if($status >= 34){
Log::channel('cron')->info('Sende kritische Erinnerung mit BCC an: ' . $user->email);
Mail::to($user->email)
->locale($user->getLocale())
->bcc(config('app.default_mail'))
->send(new MailCustomMessage($user, $data, $sender, false));
} else {
Log::channel('cron')->info('Sende normale Erinnerung an: ' . $user->email);
Mail::to($user->email)
->locale($user->getLocale())
->send(new MailCustomMessage($user, $data, $sender, false));
}
} else {
Log::channel('cron')->info('Testsystem: E-Mail-Versand simuliert für: ' . $user->email);
}
} catch(\Exception $e) {
Log::channel('cron')->error('Mail-Fehler für Benutzer ' . $user->email . ': ' . $e->getMessage());
$customer_mail->fail = true;
$customer_mail->error = $e->getMessage();
$customer_mail->save();
return 0;
}
$customer_mail->send = true;
$customer_mail->sent_at = now();
$customer_mail->save();
Log::channel('cron')->info('Erinnerungsmail erfolgreich gesendet an: ' . $user->email);
return 1;
}
}

View file

@ -3,6 +3,9 @@
namespace App\Console;
use App\Console\Commands\BusinessStore;
use App\Console\Commands\CheckPaymentsAccount;
use App\Console\Commands\UserMakeAboOrder;
use App\Console\Commands\UserCleanup;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -15,6 +18,9 @@ class Kernel extends ConsoleKernel
*/
protected $commands = [
BusinessStore::class,
CheckPaymentsAccount::class,
UserMakeAboOrder::class,
UserCleanup::class,
];
/**
@ -25,8 +31,15 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')
// ->hourly();
// Job 1: Überprüft täglich um 02:00 Uhr die Zahlungskonten.
$schedule->command('payments:check-accounts')->dailyAt('02:00');
// Jobs 2, 3, 4: Die Befehle aus deinem alten Shell-Skript.
// Werden nacheinander täglich zu unterschiedlichen Zeiten ausgeführt,
// um die Serverlast zu verteilen.
$schedule->command('business:store 0 0')->dailyAt('03:00');
$schedule->command('user:cleanup')->dailyAt('03:30');
$schedule->command('user:make_abo_order')->dailyAt('04:00');
}
/**

View file

@ -0,0 +1,662 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\CancelShipmentJob;
use App\Jobs\CreateReturnLabelJob;
use App\Jobs\TrackShipmentJob;
// Old DHL model replaced with new package model
use Acme\Dhl\Models\DhlShipment;
use App\Models\ShoppingOrder;
use App\Services\DhlModalService;
use App\Services\DhlShipmentService;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Yajra\DataTables\Facades\DataTables;
// Import new DHL package and SettingController
use Acme\Dhl\DhlManager;
/**
* DHL Shipment Controller
*
* Handles all DHL shipment operations including creation, cancellation,
* tracking, and return labels. Provides both web interface and AJAX endpoints.
*/
class DhlShipmentController extends Controller
{
/**
* Constructor
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('admin')->except(['show', 'track']);
}
/**
* Test the DHL API login credentials and return a JSON response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function testLogin()
{
try {
// Get DHL configuration with admin settings
$settingController = new \App\Http\Controllers\SettingController();
$dhlConfig = $settingController->getDhlConfig();
// Create DhlClient with merged configuration
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$dhlConfig['base_url'],
$dhlConfig['api_key'],
$dhlConfig['username'],
$dhlConfig['password']
);
// Test the connection
$connectionTest = $dhlClient->testConnection();
if ($connectionTest) {
$result = [
'success' => true,
'message' => 'DHL API Verbindung erfolgreich getestet!',
'details' => [
'base_url' => $dhlConfig['base_url'],
'using_admin_config' => !empty($dhlConfig['api_key'])
]
];
} else {
$result = [
'success' => false,
'message' => 'DHL API Verbindung fehlgeschlagen. Prüfen Sie Ihre Zugangsdaten.'
];
}
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Test login failed', [
'error' => $e->getMessage()
]);
return response()->json([
'success' => false,
'message' => 'DHL API Test fehlgeschlagen: ' . $e->getMessage()
], 500);
}
}
/**
* Display the DHL Cockpit (main overview)
*
* @param Request $request
* @return View
*/
public function index(Request $request): View
{
// Statistics for dashboard widgets
$stats = [
'total_shipments' => DhlShipment::count(),
'pending_shipments' => DhlShipment::where('status', 'pending')->count(),
'shipped_today' => DhlShipment::whereDate('created_at', today())->count(),
'returns_count' => DhlShipment::where('type', 'return')->count(),
];
return view('admin.dhl.cockpit', compact('stats'));
}
/**
* Provides data for the DHL Cockpit DataTable.
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function datatable(Request $request): JsonResponse
{
$query = DhlShipment::with(['shoppingOrder.shopping_user'])
->select('dhl_package_shipments.*') // Explicitly select to avoid conflicts
->orderBy('created_at', 'desc');
// Apply filters from the request
if ($request->filled('type')) {
$query->where('type', $request->get('type'));
}
if ($request->filled('status')) {
$query->where('status', $request->get('status'));
}
if ($request->filled('date_from')) {
$query->whereDate('created_at', '>=', $request->get('date_from'));
}
if ($request->filled('date_to')) {
$query->whereDate('created_at', '<=', $request->get('date_to'));
}
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('dhl_shipment_no', 'LIKE', "%{$search}%")
->orWhere('id', 'LIKE', "%{$search}%")
->orWhereHas('shoppingOrder', function ($orderQuery) use ($search) {
$orderQuery->where('id', $search);
});
});
}
return DataTables::eloquent($query)
->addColumn('checkbox', function ($shipment) {
return '<label class="custom-control custom-checkbox mb-0"><input type="checkbox" class="custom-control-input shipment-checkbox" value="' . $shipment->id . '"><span class="custom-control-label"></span></label>';
})
->editColumn('id', function ($shipment) {
return '<a href="' . route('admin.dhl.show', $shipment) . '" class="text-primary font-weight-semibold">#' . $shipment->id . '</a>';
})
->addColumn('type', function ($shipment) {
if ($shipment->type == 'outbound') {
return '<span class="badge badge-primary"><i class="fas fa-arrow-right"></i> Ausgehend</span>';
} else {
return '<span class="badge badge-info"><i class="fas fa-undo"></i> Retoure</span>';
}
})
->addColumn('order', function ($shipment) {
if ($shipment->order_id) {
return '<a href="' . route('admin_sales_customers_detail', $shipment->order_id) . '" class="text-primary">#' . $shipment->order_id . '</a>';
}
return '<span class="text-muted">N/A</span>';
})
->addColumn('customer', function ($shipment) {
if ($shipment->shoppingOrder && $shipment->shoppingOrder->shopping_user) {
return e($shipment->shoppingOrder->shopping_user->billing_firstname) . ' ' . e($shipment->shoppingOrder->shopping_user->billing_lastname) .
'<br><small class="text-muted">' . e($shipment->shoppingOrder->shopping_user->billing_email) . '</small>';
}
return '<span class="text-muted">Unbekannt</span>';
})
->editColumn('dhl_shipment_no', function ($shipment) {
return $shipment->dhl_shipment_no ? '<code class="text-success">' . e($shipment->dhl_shipment_no) . '</code>' : '<span class="text-muted">-</span>';
})
->addColumn('status', function ($shipment) {
$statusMap = [
'pending' => ['class' => 'warning', 'text' => 'Wartend'],
'created' => ['class' => 'success', 'text' => 'Erstellt'],
'shipped' => ['class' => 'primary', 'text' => 'Versendet'],
'delivered' => ['class' => 'info', 'text' => 'Zugestellt'],
'cancelled' => ['class' => 'secondary', 'text' => 'Storniert'],
'failed' => ['class' => 'danger', 'text' => 'Fehler'],
];
$statusInfo = $statusMap[$shipment->status] ?? ['class' => 'light', 'text' => e($shipment->status)];
return '<span class="badge badge-' . $statusInfo['class'] . '">' . $statusInfo['text'] . '</span>';
})
->addColumn('tracking_status', function ($shipment) {
if ($shipment->tracking_status) {
return '<small class="text-muted">' . e($shipment->tracking_status) . '</small>' .
($shipment->last_tracked_at ? '<br><small class="text-muted">' . $shipment->last_tracked_at->format('d.m.Y H:i') . '</small>' : '');
}
return '<span class="text-muted">-</span>';
})
->editColumn('weight_kg', function ($shipment) {
return number_format($shipment->weight_kg, 2) . ' kg';
})
->editColumn('created_at', function ($shipment) {
return $shipment->created_at->format('d.m.Y H:i');
})
->addColumn('actions', function ($shipment) {
$buttons = '<div class="btn-group" role="group">';
$buttons .= '<a href="' . route('admin.dhl.show', $shipment) . '" class="btn btn-sm btn-outline-primary" data-toggle="tooltip" title="Details anzeigen"><i class="fas fa-eye"></i></a>';
if ($shipment->label_path) {
$buttons .= '<a href="' . route('admin.dhl.download-label', $shipment) . '" class="btn btn-sm btn-outline-success" data-toggle="tooltip" title="Label herunterladen"><i class="fas fa-download"></i></a>';
}
if ($shipment->canCancel()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-warning cancel-shipment-btn" data-shipment-id="' . $shipment->id . '" data-toggle="tooltip" title="Sendung stornieren"><i class="fas fa-ban"></i></button>';
}
if ($shipment->type == 'outbound' && !$shipment->returns()->count()) {
$buttons .= '<button type="button" class="btn btn-sm btn-outline-info create-return-btn" data-shipment-id="' . $shipment->id . '" data-toggle="tooltip" title="Retourenlabel erstellen"><i class="fas fa-undo"></i></button>';
}
$buttons .= '</div>';
return $buttons;
})
->rawColumns(['checkbox', 'id', 'type', 'order', 'customer', 'dhl_shipment_no', 'status', 'tracking_status', 'actions'])
->make(true);
}
/**
* Show the form for creating a new shipment
*
* @param ShoppingOrder $order
* @return View
*/
public function create(ShoppingOrder $order): View|\Illuminate\Http\RedirectResponse
{
// Check if order already has a shipment
$existingShipment = DhlShipment::where('shopping_order_id', $order->id)
->where('type', 'outbound')
->first();
if ($existingShipment) {
return redirect()->route('admin.dhl.show', $existingShipment)
->with('warning', 'Für diese Bestellung existiert bereits eine Sendung.');
}
return view('admin.dhl.create', compact('order'));
}
/**
* Store a new shipment (async via queue)
*
* @param Request $request
* @return JsonResponse
*/
public function store(Request $request): JsonResponse
{
try {
// Use DhlModalService for validation
$dhlModalService = new DhlModalService();
$validationResult = $dhlModalService->validateShipmentData($request->all());
if (!$validationResult['valid']) {
return response()->json([
'success' => false,
'message' => 'Validierungsfehler: ' . implode(', ', $validationResult['errors'])
], 422);
}
// Basic Laravel validation as fallback
$request->validate([
'order_id' => 'required|exists:shopping_orders,id',
'weight' => 'required|numeric|min:0.1|max:31.5',
'product_code' => 'sometimes|string',
'priority' => 'sometimes|string|in:normal,high',
'auto_track' => 'sometimes|boolean',
// Shipping address validation
'shipping_firstname' => 'required|string|max:50',
'shipping_lastname' => 'required|string|max:50',
'shipping_company' => 'nullable|string|max:100',
'shipping_address' => 'required|string|max:100',
'shipping_houseNumber' => 'required|string|max:50',
'shipping_zipcode' => 'required|string|max:10',
'shipping_city' => 'required|string|max:50',
'shipping_country_id' => 'required|exists:countries,id',
'shipping_phone' => 'nullable|string|max:20',
]);
$order = ShoppingOrder::findOrFail($request->order_id);
// Check if shipment already exists
/* $existingShipment = DhlShipment::where('shopping_order_id', $order->id)
->where('type', 'outbound')
->first();
if ($existingShipment) {
return response()->json([
'success' => false,
'message' => 'Für diese Bestellung existiert bereits eine Sendung.'
], 422);
}
*/
// Use service to prepare address data
$shippingAddress = $dhlModalService->prepareAddressForApi($request->all());
// Prepare options for shipment creation
$options = [
'product_code' => $request->get('product_code', 'V01PAK'),
'priority' => $request->get('priority', 'normal'),
'auto_track' => $request->get('auto_track', true),
'shipping_address' => $shippingAddress,
'services' => $request->get('services', []),
'dimensions' => $request->only(['length', 'width', 'height'])
];
// Use DhlShipmentService (handles queue/sync automatically based on config)
$dhlShipmentService = new DhlShipmentService();
$result = $dhlShipmentService->createShipment($order, (float) $request->weight, $options);
Log::info('[DHL Controller] Shipment creation processed', [
'order_id' => $order->id,
'weight' => $request->weight,
'queued' => $result['queued'] ?? false,
'success' => $result['success'] ?? false,
]);
return response()->json($result);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch shipment creation', [
'error' => $e->getMessage(),
'order_id' => $request->order_id ?? 'unknown',
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen der Sendung: ' . $e->getMessage()
], 500);
}
}
/**
* Display the specified shipment
*
* @param DhlShipment $shipment
* @return View
*/
public function show(DhlShipment $shipment): View
{
$shipment->load(['shoppingOrder.shopping_user', 'relatedShipment']);
return view('admin.dhl.show', compact('shipment'));
}
/**
* Cancel the specified shipment
*
* @param Request $request
* @param DhlShipment $shipment
* @return JsonResponse
*/
public function cancel(Request $request, DhlShipment $shipment): JsonResponse
{
try {
// Validate cancellation is possible
if (!$shipment->canCancel()) {
return response()->json([
'success' => false,
'message' => 'Diese Sendung kann nicht mehr storniert werden.'
], 422);
}
// Dispatch cancellation job
$options = [
'priority' => $request->get('priority', 'normal')
];
CancelShipmentJob::dispatch($shipment, $options);
Log::info('[DHL Controller] Shipment cancellation job dispatched', [
'shipment_id' => $shipment->id,
'shipment_number' => $shipment->shipment_number,
]);
return response()->json([
'success' => true,
'message' => 'Sendung wird storniert...'
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch shipment cancellation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Stornieren der Sendung: ' . $e->getMessage()
], 500);
}
}
/**
* Create return label for the specified shipment
*
* @param Request $request
* @param DhlShipment $shipment
* @return JsonResponse
*/
public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse
{
try {
// Validate return label creation is possible
if ($shipment->type !== 'outbound') {
return response()->json([
'success' => false,
'message' => 'Retourenlabels können nur für ausgehende Sendungen erstellt werden.'
], 422);
}
// Check if return label already exists
$existingReturn = DhlShipment::where('related_shipment_id', $shipment->id)
->where('type', 'return')
->first();
if ($existingReturn) {
return response()->json([
'success' => false,
'message' => 'Für diese Sendung existiert bereits ein Retourenlabel.'
], 422);
}
// Dispatch return label creation job
$options = [
'auto_track' => $request->get('auto_track', false),
'priority' => $request->get('priority', 'normal')
];
CreateReturnLabelJob::dispatch($shipment, $options);
Log::info('[DHL Controller] Return label creation job dispatched', [
'original_shipment_id' => $shipment->id,
'shipment_number' => $shipment->shipment_number,
]);
return response()->json([
'success' => true,
'message' => 'Retourenlabel wird erstellt...'
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch return label creation', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Erstellen des Retourenlabels: ' . $e->getMessage()
], 500);
}
}
/**
* Update tracking status for the specified shipment
*
* @param DhlShipment $shipment
* @return JsonResponse
*/
public function updateTracking(DhlShipment $shipment): JsonResponse
{
try {
if (!$shipment->tracking_number) {
return response()->json([
'success' => false,
'message' => 'Keine Tracking-Nummer verfügbar.'
], 422);
}
// Dispatch tracking update job
TrackShipmentJob::dispatch($shipment, ['auto_retrack' => false]);
Log::info('[DHL Controller] Tracking update job dispatched', [
'shipment_id' => $shipment->id,
'tracking_number' => $shipment->tracking_number,
]);
return response()->json([
'success' => true,
'message' => 'Tracking-Informationen werden aktualisiert...'
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to dispatch tracking update', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: ' . $e->getMessage()
], 500);
}
}
/**
* Download shipping label
*
* @param DhlShipment $shipment
* @return Response
*/
public function downloadLabel(DhlShipment $shipment): Response
{
try {
if (!$shipment->label_path || !Storage::exists($shipment->label_path)) {
abort(404, 'Versandlabel nicht gefunden.');
}
$labelContent = Storage::get($shipment->label_path);
$filename = sprintf(
'dhl-label-%s-%s.pdf',
$shipment->type,
$shipment->shipment_number ?: $shipment->id
);
return response($labelContent, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', "attachment; filename=\"{$filename}\"");
} catch (Exception $e) {
Log::error('[DHL Controller] Failed to download label', [
'error' => $e->getMessage(),
'shipment_id' => $shipment->id,
'label_path' => $shipment->label_path,
]);
abort(500, 'Fehler beim Download des Versandlabels.');
}
}
/**
* Batch operations (multiple shipments)
*
* @param Request $request
* @return JsonResponse
*/
public function batchAction(Request $request): JsonResponse
{
try {
$request->validate([
'action' => 'required|in:cancel,download_labels,update_tracking',
'shipment_ids' => 'required|array|min:1',
'shipment_ids.*' => 'exists:dhl_package_shipments,id',
]);
$shipmentIds = $request->shipment_ids;
$action = $request->action;
$processed = 0;
$errors = [];
foreach ($shipmentIds as $shipmentId) {
try {
$shipment = DhlShipment::findOrFail($shipmentId);
switch ($action) {
case 'cancel':
if ($shipment->canCancel()) {
CancelShipmentJob::dispatch($shipment);
$processed++;
} else {
$errors[] = "Sendung {$shipment->shipment_number} kann nicht storniert werden.";
}
break;
case 'update_tracking':
if ($shipment->tracking_number) {
TrackShipmentJob::dispatch($shipment, ['auto_retrack' => false]);
$processed++;
} else {
$errors[] = "Sendung {$shipment->shipment_number} hat keine Tracking-Nummer.";
}
break;
case 'download_labels':
// This would require ZIP creation - implement if needed
$errors[] = "Stapel-Download noch nicht implementiert.";
break;
}
} catch (Exception $e) {
$errors[] = "Fehler bei Sendung {$shipmentId}: " . $e->getMessage();
}
}
Log::info('[DHL Controller] Batch action executed', [
'action' => $action,
'processed' => $processed,
'errors_count' => count($errors),
]);
return response()->json([
'success' => $processed > 0,
'message' => sprintf('%d Sendungen verarbeitet.', $processed),
'processed' => $processed,
'errors' => $errors,
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Batch action failed', [
'error' => $e->getMessage(),
'action' => $request->action ?? 'unknown',
]);
return response()->json([
'success' => false,
'message' => 'Fehler bei der Stapelverarbeitung: ' . $e->getMessage()
], 500);
}
}
/**
* Public tracking page (for customers)
*
* @param Request $request
* @return View|JsonResponse
*/
public function track(Request $request): View|JsonResponse
{
if ($request->expectsJson()) {
$request->validate([
'tracking_number' => 'required|string|min:10',
]);
try {
$shipment = DhlShipment::where('tracking_number', $request->tracking_number)->first();
if (!$shipment) {
return response()->json([
'success' => false,
'message' => 'Sendung nicht gefunden.'
], 404);
}
// Dispatch tracking update
TrackShipmentJob::dispatch($shipment, ['auto_retrack' => false]);
return response()->json([
'success' => true,
'data' => [
'tracking_number' => $shipment->tracking_number,
'status' => $shipment->status,
'tracking_status' => $shipment->tracking_status,
'last_tracked_at' => $shipment->last_tracked_at?->format('d.m.Y H:i'),
]
]);
} catch (Exception $e) {
Log::error('[DHL Controller] Public tracking failed', [
'error' => $e->getMessage(),
'tracking_number' => $request->tracking_number ?? 'unknown',
]);
return response()->json([
'success' => false,
'message' => 'Fehler beim Abrufen der Tracking-Informationen.'
], 500);
}
}
return view('public.tracking');
}
}

View file

@ -130,24 +130,23 @@ class FileController extends Controller
if(!Storage::disk($disk)->exists($path)){
return Response::make('Datei nicht gefunden.', 404);
}
if ($do === 'download') {
return Storage::disk($disk)->download($path, $filename);
}
$file = Storage::disk($disk)->get($path);
$mime = Storage::disk($disk)->mimeType($path);
if(isset($file)){
if($do === 'download'){
return Response::make($file, 200)
->header("Content-Type", $mime)
->header('Content-disposition', 'attachment; filename="'.$filename.'"');
}
if($do === 'stream'){
return Response::make($file, 200)
->header("Content-Type", $mime)
->header('Content-disposition','inline; filename="'.$filename.'"');
return Storage::disk($disk)->response($path, $filename);
}
if($do === 'file'){
return Response::make($file, 200)
->header("Content-Type", $mime)
->header("Content-Length", strlen($file))
->header('Content-disposition', 'filename="'.$filename.'"');
}
if($do === 'image'){

View file

@ -17,6 +17,7 @@ use App\Models\ShoppingOrder;
use App\Models\UserSalesVolume;
use App\Services\BusinessPlan\TreeCalcBot;
use App\Services\BusinessPlan\TreeCalcBotOptimized;
use App\Services\DhlModalService;
class ModalController extends Controller
{
@ -171,6 +172,11 @@ class ModalController extends Controller
$ret = view("user.abo.modal_abo_show_products", compact( 'data', 'user_abo'))->render();
}
if($data['action'] === 'create-dhl-shipment') {
$id = $data['id'] ?? null;
$ret = $this->handleDhlShipmentModal($id, $data);
}
}
return response()->json(['response' => $data, 'html'=>$ret, 'status'=>$status]);
}
@ -195,6 +201,55 @@ class ModalController extends Controller
}
/**
* Handle DHL shipment modal preparation
*
* @param mixed $id Order ID or 'new'
* @param array $data Request data
* @return string Rendered view
*/
private function handleDhlShipmentModal($id, array $data): string
{
try {
$dhlModalService = new DhlModalService();
$modalData = $dhlModalService->prepareModalData($id, $data);
// Merge the prepared data with the original request data
$viewData = array_merge($data, $modalData, [
'id' => $id,
'data' => $data
]);
return view("admin.dhl.modal_create_shipment", $viewData)->render();
} catch (\Exception $e) {
\Log::error('[ModalController] Error in DHL shipment modal', [
'order_id' => $id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Return error view or fallback
$errorData = [
'id' => $id,
'data' => $data,
'order' => null,
'orderWeight' => 1.0,
'shippingAddress' => null,
'availableCountries' => \App\Models\Country::where('active', 1)->get(),
'productCodes' => [
'V01PAK' => 'DHL Paket (National)',
'V53WPAK' => 'DHL Paket International',
'V54EPAK' => 'DHL Express'
],
'errors' => ['Fehler beim Laden der Daten: ' . $e->getMessage()],
'warnings' => []
];
return view("admin.dhl.modal_create_shipment", $errorData)->render();
}
}
}

View file

@ -65,16 +65,16 @@ class InController extends Controller
$data = Request::all();
$response = "";
$status = false;
if(isset($data['action']) && $data['action'] === 'user-order-show-product'){
if(isset($data['action'])){
if($data['action'] === 'user-order-show-product'){
$product = Product::find($data['id']); //current user form order
$ret = view("admin.modal.show_product", compact('product', 'data'))->render();
return response()->json(['response' => $data, 'html'=>$ret, 'status'=>$status]);
}
}
$data = Request::get('data');
$target = Request::get('target');
if($data === "data_protection"){

View file

@ -27,19 +27,92 @@ class SettingController extends Controller
public function store()
{
$data = Request::all();
if(isset($data['action'])){
if(isset($data['settings'])){
foreach ($data['settings'] as $key=>$value){
if (isset($data['action'])) {
if (isset($data['settings'])) {
foreach ($data['settings'] as $key => $value) {
$value['val'] = isset($value['val']) ? $value['val'] : false;
Setting::setContentBySlug($key, $value['val'], $value['type']);
}
}
}
// DHL-spezifische Behandlung
if ($data['action'] === 'save_dhl') {
$this->updateDhlConfigCache();
\Session()->flash('alert-save-dhl', 'DHL Konfiguration erfolgreich gespeichert!');
} else {
\Session()->flash('alert-save', '1');
}
}
return redirect(route('admin_settings'));
}
/**
* Get DHL configuration merged from database settings and .env values
* Database settings override .env values
*/
public function getDhlConfig()
{
return [
// API Settings
'base_url' => Setting::getContentBySlug('dhl_base_url') ?: config('dhl.base_url'),
'api_key' => Setting::getContentBySlug('dhl_api_key') ?: config('dhl.api_key'),
'username' => Setting::getContentBySlug('dhl_username') ?: config('dhl.username'),
'password' => Setting::getContentBySlug('dhl_password') ?: config('dhl.password'),
'billing_number' => Setting::getContentBySlug('dhl_billing_number') ?: config('dhl.billing_number'),
// Product Settings
'default_product' => Setting::getContentBySlug('dhl_product') ?: config('dhl.default_product'),
'label_format' => Setting::getContentBySlug('dhl_label_format') ?: config('dhl.label_format'),
'print_format' => Setting::getContentBySlug('dhl_print_format') ?: config('dhl.print_format'),
'retoure_print_format' => Setting::getContentBySlug('dhl_retoure_print_format') ?: config('dhl.retoure_print_format'),
'use_queue' => Setting::getContentBySlug('dhl_use_queue') ?: config('dhl.use_queue'),
// Sender Address
'sender' => [
'company' => Setting::getContentBySlug('dhl_sender_company') ?: config('dhl.sender.company'),
'name' => Setting::getContentBySlug('dhl_sender_name') ?: config('dhl.sender.name'),
'street' => Setting::getContentBySlug('dhl_sender_street') ?: config('dhl.sender.street'),
'houseNumber' => Setting::getContentBySlug('dhl_sender_house_number') ?: config('dhl.sender.houseNumber'),
'postalCode' => Setting::getContentBySlug('dhl_sender_postal_code') ?: config('dhl.sender.postalCode'),
'city' => Setting::getContentBySlug('dhl_sender_city') ?: config('dhl.sender.city'),
'country' => Setting::getContentBySlug('dhl_sender_country') ?: config('dhl.sender.country'),
'email' => Setting::getContentBySlug('dhl_sender_email') ?: config('dhl.sender.email'),
'phone' => Setting::getContentBySlug('dhl_sender_phone') ?: config('dhl.sender.phone'),
],
// Account Numbers
'account_numbers' => [
'V01PAK' => Setting::getContentBySlug('dhl_account_v01pak') ?: config('dhl.account_numbers.V01PAK'),
'V62WP' => Setting::getContentBySlug('dhl_account_v62wp') ?: config('dhl.account_numbers.V62WP'),
'V53PAK' => Setting::getContentBySlug('dhl_account_v53pak') ?: config('dhl.account_numbers.V53PAK'),
'V07PAK' => Setting::getContentBySlug('dhl_account_v07pak') ?: config('dhl.account_numbers.V07PAK'),
'default' => config('dhl.account_numbers.default'),
],
// Static config values (webhook, profile, legacy)
'profile' => config('dhl.profile'),
'webhook' => config('dhl.webhook'),
'legacy' => config('dhl.legacy'),
];
}
/**
* Update DHL configuration cache after saving settings
*/
private function updateDhlConfigCache()
{
// Clear config cache to force reload from database
\Artisan::call('config:clear');
// Optional: Test DHL connection with new settings
try {
$dhlManager = app('Acme\Dhl\DhlManager');
// You could add a connection test here if needed
\Log::info('DHL configuration updated successfully');
} catch (\Exception $e) {
\Log::error('DHL configuration update failed: ' . $e->getMessage());
}
}
}

View file

@ -322,6 +322,9 @@ class UserShopController extends Controller
return redirect()->back()->withErrors($validator)->withInput(Request::all());
}
\Session()->flash('shop-name-error', 'check');
if(Request::get('user_shop_id')){
return back()->withInput(Request::all());
}
return redirect(route('user_shop'))->withInput(Request::all());
}
@ -329,6 +332,8 @@ class UserShopController extends Controller
$rules = array(
'user_shop_name' => ' required|alpha_dash|unique:user_shops,name|min:4|max:20|full_word_check',
'user_shop_active' => 'accepted',
);
Validator::extend('full_word_check', function ($attribute, $value, $parameters, $validator) {
if(in_array($value, config('profanity.full_word_check'))){
@ -337,25 +342,25 @@ class UserShopController extends Controller
return true;
});
$validator = Validator::make(Request::all(), $rules);
if ($validator->fails()) {
\Session()->flash('shop-name-error', 'error');
}else{
\Session()->flash('shop-name-error', 'check');
}
$rules = array(
'user_shop_active' => 'accepted',
);
$validator = Validator::make(Request::all(), $rules);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput(Request::all());
}
\Session()->flash('shop-name-error', 'check');
//all is right - save
$user = Auth::user();
$data = Request::all();
$slug = SlugService::createSlug(UserShop::class, 'slug', $data['user_shop_name']);
if(isset($data['user_shop_id'])){
$user_shop = UserShop::find($data['user_shop_id']);
if($user_shop->user_id != $user->id){
abort(404);
}
$user_shop->name = $slug;
$user_shop->slug = $slug;
$user_shop->save();
}else{
$user_shop = UserShop::create([
'user_id' => $user->id,
'name' => $slug,
@ -363,15 +368,17 @@ class UserShopController extends Controller
'active_date' => now(),
]
);
$ret = $this->userShopRegisterSubDomain($user_shop->slug);
}
\Session()->flash('alert-save', true);
return redirect(route('user_shop'));
/*$ret = $this->userShopRegisterSubDomain($user_shop->slug);
if($ret['success'] === true){
\Session()->flash('alert-save', true);
}else{
$user_shop->forceDelete();
\Session()->flash('alert-error', $ret['error']);
}
return redirect(route('user_shop'));
return redirect(route('user_shop'));*/
}
}
@ -444,4 +451,19 @@ class UserShopController extends Controller
));
}
public function editName(){
$user = Auth::user();
$user_shop = $user->shop;
if(!$user_shop){
abort(404);
}
$user_shop_domain = $user_shop->getSubdomain(false);
$data = [
'user' => $user,
'user_shop_id' => $user_shop->id,
'user_shop_domain' => $user_shop_domain,
];
return view('user.shop_edit_name', $data);
}
}

View file

@ -37,6 +37,7 @@ class DomainResolver
// leiten wir sicher auf die Hauptdomain um.
if ($context->isUnknown()) {
// Detailliertes Logging für spätere Analyse
if(config('app.debug')){
\Log::warning('Unknown domain accessed', [
'host' => $request->getHost(),
'subdomain' => $context->subdomain,
@ -45,16 +46,17 @@ class DomainResolver
'referer' => $request->header('referer'),
'path' => $request->getPathInfo()
]);
}
// Holt die URL der Hauptdomain vom DomainService und leitet um.
$mainUrl = app(\App\Services\DomainService::class)->buildUrl('main');
return redirect()->away($mainUrl, 301);
}
if(config('app.debug')){
\Log::debug('DomainResolver: context', [
'context' => $context,
'subdomain' => $context->subdomain
]);
}
// Für User-Shop-Domains: Validierung und Route-Parameter-Bereinigung
if ($context->isUserShop()) {
// Validiere UserShop-Berechtigung (bereits im DomainServiceProvider geprüft,

View file

@ -0,0 +1,144 @@
<?php
namespace App\Jobs;
use App\Models\DhlShipment;
use App\Services\DhlApiService;
use Exception;
use Illuminate\Bus\Queueable as BusQueueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* Job to cancel DHL shipments asynchronously
*
* This job handles the cancellation of DHL shipments in the background,
* preventing API timeouts and improving user experience.
*/
class CancelShipmentJob implements ShouldQueue
{
use BusQueueable, Dispatchable, InteractsWithQueue, SerializesModels;
/**
* @var DhlShipment
*/
public $dhlShipment;
/**
* @var array
*/
public $options;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* The maximum number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 60;
/**
* Create a new job instance.
*
* @param DhlShipment $dhlShipment
* @param array $options
*/
public function __construct(DhlShipment $dhlShipment, array $options = [])
{
$this->dhlShipment = $dhlShipment;
$this->options = $options;
// Set queue name based on priority
if (isset($options['priority']) && $options['priority'] === 'high') {
$this->onQueue('high-priority');
} else {
$this->onQueue('dhl-cancellations');
}
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
Log::info('[DHL Queue] Starting shipment cancellation job', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'attempt' => $this->attempts(),
]);
$dhlService = new DhlApiService();
// Cancel the shipment
$success = $dhlService->cancelShipment($this->dhlShipment);
if ($success) {
Log::info('[DHL Queue] Shipment cancelled successfully', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
]);
} else {
throw new Exception('Cancellation returned false');
}
} catch (Exception $e) {
Log::error('[DHL Queue] Shipment cancellation failed', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'error' => $e->getMessage(),
'attempt' => $this->attempts(),
'max_tries' => $this->tries,
]);
// If this is the final attempt, mark as permanently failed
if ($this->attempts() >= $this->tries) {
Log::error('[DHL Queue] Shipment cancellation permanently failed', [
'shipment_id' => $this->dhlShipment->id,
'error' => $e->getMessage(),
]);
}
throw $e; // Re-throw to trigger retry mechanism
}
}
/**
* Handle a job failure.
*
* @param Exception $exception
*/
public function failed(Exception $exception): void
{
Log::error('[DHL Queue] CancelShipmentJob permanently failed', [
'shipment_id' => $this->dhlShipment->id,
'shipment_number' => $this->dhlShipment->shipment_number,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// You could implement additional failure handling here:
// - Send notification to admin
// - Create manual task for staff to handle cancellation
// - Update shipment status to indicate cancellation failed
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
return now()->addHour(); // Shorter timeout for cancellations
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace App\Jobs;
use App\Models\DhlShipment;
use App\Services\DhlApiService;
use Exception;
use Illuminate\Bus\Queueable as BusQueueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* Job to create DHL return labels asynchronously
*
* This job handles the creation of DHL return labels in the background,
* preventing API timeouts and improving user experience.
*/
class CreateReturnLabelJob implements ShouldQueue
{
use BusQueueable, Dispatchable, InteractsWithQueue, SerializesModels;
/**
* @var DhlShipment
*/
public $originalShipment;
/**
* @var array
*/
public $options;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* The maximum number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 120;
/**
* Create a new job instance.
*
* @param DhlShipment $originalShipment
* @param array $options
*/
public function __construct(DhlShipment $originalShipment, array $options = [])
{
$this->originalShipment = $originalShipment;
$this->options = $options;
// Set queue name based on priority
if (isset($options['priority']) && $options['priority'] === 'high') {
$this->onQueue('high-priority');
} else {
$this->onQueue('dhl-returns');
}
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
Log::info('[DHL Queue] Starting return label creation job', [
'original_shipment_id' => $this->originalShipment->id,
'original_shipment_number' => $this->originalShipment->shipment_number,
'attempt' => $this->attempts(),
]);
$dhlService = new DhlApiService();
// Create the return label
$returnShipment = $dhlService->createReturnLabel(
$this->originalShipment,
$this->options
);
Log::info('[DHL Queue] Return label created successfully', [
'original_shipment_id' => $this->originalShipment->id,
'return_shipment_id' => $returnShipment->id,
'return_shipment_number' => $returnShipment->shipment_number,
]);
// Trigger follow-up actions if specified
if (isset($this->options['auto_track']) && $this->options['auto_track']) {
\App\Jobs\TrackShipmentJob::dispatch($returnShipment)->delay(now()->addMinutes(5));
}
} catch (Exception $e) {
Log::error('[DHL Queue] Return label creation failed', [
'original_shipment_id' => $this->originalShipment->id,
'error' => $e->getMessage(),
'attempt' => $this->attempts(),
'max_tries' => $this->tries,
]);
// If this is the final attempt, mark as permanently failed
if ($this->attempts() >= $this->tries) {
Log::error('[DHL Queue] Return label creation permanently failed', [
'original_shipment_id' => $this->originalShipment->id,
'error' => $e->getMessage(),
]);
}
throw $e; // Re-throw to trigger retry mechanism
}
}
/**
* Handle a job failure.
*
* @param Exception $exception
*/
public function failed(Exception $exception): void
{
Log::error('[DHL Queue] CreateReturnLabelJob permanently failed', [
'original_shipment_id' => $this->originalShipment->id,
'original_shipment_number' => $this->originalShipment->shipment_number,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// You could implement additional failure handling here:
// - Send notification to admin
// - Create manual task for staff to handle return label creation
// - Update original shipment to mark return label creation failed
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
return now()->addHours(2);
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace App\Jobs;
use App\Models\ShoppingOrder;
use App\Services\DhlDataHelper;
use Exception;
use Illuminate\Bus\Queueable as BusQueueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* Job to create DHL shipments asynchronously
*
* This job handles the creation of DHL shipments in the background,
* preventing API timeouts and improving user experience.
*/
class CreateShipmentJob implements ShouldQueue
{
use BusQueueable, Dispatchable, InteractsWithQueue, SerializesModels;
/**
* @var ShoppingOrder
*/
public $shoppingOrder;
/**
* @var float
*/
public $weight;
/**
* @var array
*/
public $options;
/**
* @var array
*/
public $dhlConfig;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* The maximum number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 120;
/**
* Create a new job instance.
*
* @param ShoppingOrder $shoppingOrder
* @param float $weight
* @param array $options
* @param array|null $dhlConfig
*/
public function __construct(ShoppingOrder $shoppingOrder, float $weight = 1.0, array $options = [], array $dhlConfig = [])
{
$this->shoppingOrder = $shoppingOrder;
$this->weight = $weight;
$this->options = $options;
// Load DHL config once when creating the job
if (empty($dhlConfig)) {
$settingController = new \App\Http\Controllers\SettingController();
$this->dhlConfig = $settingController->getDhlConfig();
} else {
$this->dhlConfig = $dhlConfig;
}
// Set queue name based on priority
if (isset($options['priority']) && $options['priority'] === 'high') {
$this->onQueue('high-priority');
} else {
$this->onQueue('dhl-shipments');
}
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
Log::info('[DHL Queue] Starting shipment creation job', [
'order_id' => $this->shoppingOrder->id,
'weight' => $this->weight,
'attempt' => $this->attempts(),
]);
// Use DHL configuration loaded in constructor
$dhlClient = new \Acme\Dhl\Support\DhlClient(
$this->dhlConfig['base_url'],
$this->dhlConfig['api_key'],
$this->dhlConfig['username'],
$this->dhlConfig['password']
);
$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
// Prepare order data using helper
$orderData = DhlDataHelper::prepareOrderData($this->shoppingOrder, $this->weight, $this->options, $this->dhlConfig);
// Create the shipment using new package
$result = $shippingService->createLabel($orderData);
Log::info('[DHL Queue] Shipment created successfully', [
'order_id' => $this->shoppingOrder->id,
'shipment_number' => $result['shipmentNumber'] ?? 'N/A',
'label_path' => $result['labelPath'] ?? 'N/A',
]);
// Trigger follow-up actions if specified (if tracking number available)
if (isset($this->options['auto_track']) && $this->options['auto_track'] && !empty($result['trackingNumber'])) {
Log::info('[DHL Queue] Scheduling tracking update', [
'tracking_number' => $result['trackingNumber']
]);
// Note: TrackShipmentJob would need to be updated to work with tracking numbers
}
} catch (Exception $e) {
Log::error('[DHL Queue] Shipment creation failed', [
'order_id' => $this->shoppingOrder->id,
'error' => $e->getMessage(),
'attempt' => $this->attempts(),
'max_tries' => $this->tries,
]);
// If this is the final attempt, mark as permanently failed
if ($this->attempts() >= $this->tries) {
Log::error('[DHL Queue] Shipment creation permanently failed', [
'order_id' => $this->shoppingOrder->id,
'error' => $e->getMessage(),
]);
}
throw $e; // Re-throw to trigger retry mechanism
}
}
/**
* Handle a job failure.
*
* @param Exception $exception
*/
public function failed(Exception $exception): void
{
Log::error('[DHL Queue] CreateShipmentJob permanently failed', [
'order_id' => $this->shoppingOrder->id,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
// You could implement additional failure handling here:
// - Send notification to admin
// - Update order status
// - Create manual task for staff
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
return now()->addHours(2);
}
}

View file

@ -0,0 +1,194 @@
<?php
namespace App\Jobs;
use App\Models\DhlShipment;
use App\Services\DhlApiService;
use Exception;
use Illuminate\Bus\Queueable as BusQueueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
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.
*/
class TrackShipmentJob implements ShouldQueue
{
use BusQueueable, Dispatchable, InteractsWithQueue, SerializesModels;
/**
* @var DhlShipment
*/
public $dhlShipment;
/**
* @var array
*/
public $options;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 2; // Lower tries for tracking as it's less critical
/**
* The maximum number of seconds the job can run before timing out.
*
* @var int
*/
public $timeout = 60;
/**
* Create a new job instance.
*
* @param DhlShipment $dhlShipment
* @param array $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');
}
/**
* Execute the job.
*/
public function handle(): void
{
try {
Log::info('[DHL Queue] Starting shipment tracking job', [
'shipment_id' => $this->dhlShipment->id,
'tracking_number' => $this->dhlShipment->tracking_number,
'attempt' => $this->attempts(),
]);
$dhlService = new DhlApiService();
// Get tracking details
$trackingDetails = $dhlService->getTrackingDetails($this->dhlShipment);
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,
]);
// 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->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,
]);
}
}
} catch (Exception $e) {
Log::warning('[DHL Queue] Shipment tracking failed', [
'shipment_id' => $this->dhlShipment->id,
'tracking_number' => $this->dhlShipment->tracking_number,
'error' => $e->getMessage(),
'attempt' => $this->attempts(),
'max_tries' => $this->tries,
]);
// For tracking, we don't necessarily need to fail hard
if ($this->attempts() >= $this->tries) {
Log::warning('[DHL Queue] Shipment tracking permanently failed', [
'shipment_id' => $this->dhlShipment->id,
'error' => $e->getMessage(),
]);
// Don't re-throw for final attempt - just log and continue
return;
}
throw $e; // Re-throw to trigger retry mechanism
}
}
/**
* 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,
'error' => $exception->getMessage(),
]);
// Tracking failures are less critical - just log them
}
/**
* 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);
}
/**
* 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
{
switch (strtolower($status)) {
case 'picked_up':
case 'in_transit':
return 120; // 2 hours for active shipments
case 'out_for_delivery':
return 60; // 1 hour when out for delivery
case 'exception':
case 'failed_attempt':
return 240; // 4 hours for problem shipments
default:
return 180; // 3 hours default
}
}
/**
* Determine the time at which the job should timeout.
*
* @return \DateTime
*/
public function retryUntil()
{
return now()->addMinutes(30); // Short timeout for tracking
}
}

View file

@ -51,7 +51,7 @@ class MailAutoReleaseAccount extends Mailable
$copy1line .= 'Sollte Daten nicht vollständig sein, bitte Kontakt zum Berater aufnehmen.'."\n";
return $this->view('emails.info')->with([
'url' => route('admin_lead_edit', $this->user->id).'?show=check_lead',
'url' => config('app.url_crm') . '/admin/lead/edit/' . $this->user->id . '?show=check_lead',
'title' => 'Berater Registrierung prüfen',
'button' => 'zur Berater Prüfung',
'copy1line' => $copy1line,

View file

@ -61,14 +61,14 @@ class MailInfo extends Mailable
$copy1line = "Infos zum Berater:"."\n";
$button = "zum Berater";
$title = "Ein Berater möchte seine Mitgliedschaft beenden.";
$url = route('admin_lead_edit', $this->user->id).'?show=check_lead';
$url = config('app.url_crm') . '/admin/lead/edit/' . $this->user->id . '?show=check_lead';
}
if($this->action === "check_is_like_customer") {
$copy1line = "Hier geht es zum Kunden:"."\n";
$button = "zum Kunden";
$title = "Ein Kunden muss überprüft werden und einem Berater zugeordnet werden, da die Adresse nicht eindeutig ist.";
$url = route('admin_customer_detail', $this->user->id);
$url = config('app.url_crm') . '/admin/customer/detail/' . $this->user->id;
$content .= $this->user ? 'Firma: '.$this->user->billing_company."\n" : '';
$content .= \App\Services\HTMLHelper::getSalutationLang($this->user->billing_salutation)." ";
$content .= $this->user->billing_firstname." ";
@ -86,7 +86,7 @@ class MailInfo extends Mailable
$copy1line = "Hier geht es zum Kunden:"."\n";
$button = "zum Kunden";
$title = "Ein Kunden muss erneut überprüft werden, da bei einer Änderung eine bestehende Kundenhoheit gefunden wurde.";
$url = route('admin_customer_detail', $this->user->id);
$url = config('app.url_crm') . '/admin/customer/detail/' . $this->user->id;
$content .= "Folgende Daten für die Kundenhoheit wurden geändert:"."\n";
foreach ($this->data as $key=>$value){
$content .= $this->user->{$key}." => ".$value."\n";

View file

@ -52,7 +52,7 @@ class MailReleaseAccount extends Mailable
$copy1line .= 'Der Berater erhält eine Mail, dass sein Account freigeschaltet wurde. Der Vertrag wird automatisch mit den Daten des Vertriebspartners erstellt.'."\n";
return $this->view('emails.info')->with([
'url' => route('admin_lead_edit', $this->user->id).'?show=check_lead',
'url' => config('app.url_crm') . '/admin/lead/edit/' . $this->user->id . '?show=check_lead',
'title' => 'Berater Registrierung prüfen',
'button' => 'zur Berater Prüfung',
'copy1line' => $copy1line,

View file

@ -48,7 +48,7 @@ class MailReleaseDocument extends Mailable
$copy1line .= 'Bei fehlerhafter Angabe nimm bitte kontakt mit dem Berater auf.'."\n";
return $this->view('emails.info')->with([
'url' => route('admin_lead_edit', $this->user->id).'?show=check_lead',
'url' => config('app.url_crm') . '/admin/lead/edit/' . $this->user->id . '?show=check_lead',
'title' => 'Berater Unterlagen prüfen',
'button' => 'zur Berater Prüfung',
'copy1line' => $copy1line,

407
app/Models/DhlShipment.php Normal file
View file

@ -0,0 +1,407 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Carbon;
/**
* DHL Shipment Model
*
* Represents a DHL shipment for a shopping order, including both outbound and return shipments.
*
* @property int $id
* @property int $shopping_order_id
* @property string|null $shipment_number DHL shipment number
* @property string|null $tracking_number DHL tracking number
* @property string $type Type: 'outbound' or 'return'
* @property int|null $related_shipment_id For returns: reference to original shipment
* @property float $weight Package weight in kg
* @property int|null $length Package length in cm
* @property int|null $width Package width in cm
* @property int|null $height Package height in cm
* @property string $product_code DHL product code (e.g., V01PAK)
* @property array|null $services Additional DHL services
* @property string|null $label_path Path to generated label file
* @property string $label_format Label format (PDF or ZPL)
* @property bool $label_printed Whether label has been printed
* @property string $status Shipment status
* @property string|null $tracking_status Current tracking status from DHL
* @property string|null $tracking_details Detailed tracking information (JSON)
* @property Carbon|null $last_tracked_at Last tracking update
* @property string $recipient_name Recipient name
* @property string|null $recipient_company Recipient company
* @property string $recipient_street Recipient street
* @property string $recipient_street_number Recipient street number
* @property string $recipient_postal_code Recipient postal code
* @property string $recipient_city Recipient city
* @property string|null $recipient_state Recipient state
* @property string $recipient_country Recipient country code
* @property string|null $recipient_email Recipient email
* @property string|null $recipient_phone Recipient phone
* @property array|null $api_request_data API request data for debugging
* @property array|null $api_response_data API response data for debugging
* @property string|null $api_errors API error messages
* @property float|null $shipping_cost Shipping cost
* @property string $currency Currency code
* @property string|null $notes Internal notes
* @property array|null $metadata Additional metadata
* @property Carbon|null $shipped_at When the package was shipped
* @property Carbon|null $delivered_at When the package was delivered
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property-read ShoppingOrder $shoppingOrder
* @property-read DhlShipment|null $relatedShipment
* @property-read DhlShipment|null $returnShipment
* @property-read string|null $dimensions
* @property-read string|null $label_url
* @property-read string $recipient_address
* @property-read string $status_label
* @property-read string $type_label
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment active()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment outbound()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment returns()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment trackable()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereApiErrors($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereApiRequestData($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereApiResponseData($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereCurrency($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereDeliveredAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereHeight($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereLabelFormat($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereLabelPath($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereLabelPrinted($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereLastTrackedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereLength($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereMetadata($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereNotes($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereProductCode($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientCity($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientCompany($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientCountry($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientPhone($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientPostalCode($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientState($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientStreet($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRecipientStreetNumber($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereRelatedShipmentId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereServices($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereShipmentNumber($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereShippedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereShippingCost($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereShoppingOrderId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereTrackingDetails($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereTrackingNumber($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereTrackingStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereWeight($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DhlShipment whereWidth($value)
* @mixin \Eloquent
*/
class DhlShipment extends Model
{
use HasFactory;
/**
* The table associated with the model.
*/
protected $table = 'dhl_shipments';
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'shopping_order_id',
'shipment_number',
'tracking_number',
'type',
'related_shipment_id',
'weight',
'length',
'width',
'height',
'product_code',
'services',
'label_path',
'label_format',
'label_printed',
'status',
'tracking_status',
'tracking_details',
'last_tracked_at',
'recipient_name',
'recipient_company',
'recipient_street',
'recipient_street_number',
'recipient_postal_code',
'recipient_city',
'recipient_state',
'recipient_country',
'recipient_email',
'recipient_phone',
'api_request_data',
'api_response_data',
'api_errors',
'shipping_cost',
'currency',
'notes',
'metadata',
'shipped_at',
'delivered_at',
];
/**
* The attributes that should be cast.
*/
protected $casts = [
'weight' => 'float',
'length' => 'integer',
'width' => 'integer',
'height' => 'integer',
'services' => 'array',
'label_printed' => 'boolean',
'tracking_details' => 'array',
'last_tracked_at' => 'datetime',
'api_request_data' => 'array',
'api_response_data' => 'array',
'shipping_cost' => 'decimal:2',
'metadata' => 'array',
'shipped_at' => 'datetime',
'delivered_at' => 'datetime',
];
/**
* Shipment types
*/
public const TYPE_OUTBOUND = 'outbound';
public const TYPE_RETURN = 'return';
/**
* Shipment statuses
*/
public const STATUS_CREATED = 'created';
public const STATUS_SUBMITTED = 'submitted';
public const STATUS_IN_TRANSIT = 'in_transit';
public const STATUS_DELIVERED = 'delivered';
public const STATUS_RETURNED = 'returned';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_FAILED = 'failed';
/**
* Get the shopping order that owns the shipment
*/
public function shoppingOrder(): BelongsTo
{
return $this->belongsTo(ShoppingOrder::class, 'shopping_order_id');
}
/**
* Get the related shipment (for returns)
*/
public function relatedShipment(): BelongsTo
{
return $this->belongsTo(DhlShipment::class, 'related_shipment_id');
}
/**
* Get the return shipment (for outbound shipments)
*/
public function returnShipment(): HasOne
{
return $this->hasOne(DhlShipment::class, 'related_shipment_id');
}
/**
* Scope for outbound shipments
*/
public function scopeOutbound($query)
{
return $query->where('type', self::TYPE_OUTBOUND);
}
/**
* Scope for return shipments
*/
public function scopeReturns($query)
{
return $query->where('type', self::TYPE_RETURN);
}
/**
* Scope for active shipments (not cancelled or failed)
*/
public function scopeActive($query)
{
return $query->whereNotIn('status', [self::STATUS_CANCELLED, self::STATUS_FAILED]);
}
/**
* Scope for trackable shipments (have tracking number)
*/
public function scopeTrackable($query)
{
return $query->whereNotNull('tracking_number');
}
/**
* Check if shipment is outbound
*/
public function isOutbound(): bool
{
return $this->type === self::TYPE_OUTBOUND;
}
/**
* Check if shipment is return
*/
public function isReturn(): bool
{
return $this->type === self::TYPE_RETURN;
}
/**
* Check if shipment can be cancelled
*/
public function canBeCancelled(): bool
{
return in_array($this->status, [
self::STATUS_CREATED,
self::STATUS_SUBMITTED,
]);
}
/**
* Check if shipment is delivered
*/
public function isDelivered(): bool
{
return $this->status === self::STATUS_DELIVERED;
}
/**
* Check if shipment has tracking information
*/
public function hasTracking(): bool
{
return !empty($this->tracking_number);
}
/**
* Check if label is available
*/
public function hasLabel(): bool
{
return !empty($this->label_path) && file_exists(storage_path('app/' . $this->label_path));
}
/**
* Get full recipient address as formatted string
*/
public function getRecipientAddressAttribute(): string
{
$address = $this->recipient_name;
if ($this->recipient_company) {
$address .= "\n" . $this->recipient_company;
}
$address .= "\n" . $this->recipient_street . ' ' . $this->recipient_street_number;
$address .= "\n" . $this->recipient_postal_code . ' ' . $this->recipient_city;
if ($this->recipient_state) {
$address .= "\n" . $this->recipient_state;
}
$address .= "\n" . $this->recipient_country;
return $address;
}
/**
* Get package dimensions as formatted string
*/
public function getDimensionsAttribute(): ?string
{
if (!$this->length || !$this->width || !$this->height) {
return null;
}
return $this->length . ' x ' . $this->width . ' x ' . $this->height . ' cm';
}
/**
* Get human-readable status
*/
public function getStatusLabelAttribute(): string
{
return match($this->status) {
self::STATUS_CREATED => 'Erstellt',
self::STATUS_SUBMITTED => 'Übertragen',
self::STATUS_IN_TRANSIT => 'Unterwegs',
self::STATUS_DELIVERED => 'Zugestellt',
self::STATUS_RETURNED => 'Zurückgeschickt',
self::STATUS_CANCELLED => 'Storniert',
self::STATUS_FAILED => 'Fehler',
default => 'Unbekannt',
};
}
/**
* Get human-readable type
*/
public function getTypeLabelAttribute(): string
{
return match($this->type) {
self::TYPE_OUTBOUND => 'Versand',
self::TYPE_RETURN => 'Retoure',
default => 'Unbekannt',
};
}
/**
* Get label file URL for download
*/
public function getLabelUrlAttribute(): ?string
{
if (!$this->hasLabel()) {
return null;
}
return route('admin.dhl.shipments.label', $this->id);
}
/**
* Boot the model
*/
protected static function boot()
{
parent::boot();
static::creating(function ($shipment) {
// Set default values
if (empty($shipment->currency)) {
$shipment->currency = config('dhl.defaults.currency', 'EUR');
}
if (empty($shipment->product_code)) {
$shipment->product_code = config('dhl.defaults.product', 'V01PAK');
}
if (empty($shipment->label_format)) {
$shipment->label_format = config('dhl.labels.format', 'PDF');
}
});
}
}

View file

@ -101,6 +101,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $abo_interval
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereAboInterval($value)
* @method static \Illuminate\Database\Eloquent\Builder|ShoppingOrder whereIsAbo($value)
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\DhlShipment> $dhlOutboundShipments
* @property-read int|null $dhl_outbound_shipments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\DhlShipment> $dhlReturnShipments
* @property-read int|null $dhl_return_shipments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\DhlShipment> $dhlShipments
* @property-read int|null $dhl_shipments_count
* @mixin \Eloquent
*/
class ShoppingOrder extends Model
@ -623,4 +629,44 @@ class ShoppingOrder extends Model
return $ret;
}
/**
* Get DHL shipments for this order
*/
public function dhlShipments()
{
return $this->hasMany('App\Models\DhlShipment', 'shopping_order_id');
}
/**
* Get outbound DHL shipments only
*/
public function dhlOutboundShipments()
{
return $this->hasMany('App\Models\DhlShipment', 'shopping_order_id')->where('type', 'outbound');
}
/**
* Get return DHL shipments only
*/
public function dhlReturnShipments()
{
return $this->hasMany('App\Models\DhlShipment', 'shopping_order_id')->where('type', 'return');
}
/**
* Check if order has DHL shipments
*/
public function hasDhlShipments(): bool
{
return $this->dhlShipments()->exists();
}
/**
* Get latest DHL shipment
*/
public function getLatestDhlShipment()
{
return $this->dhlShipments()->latest()->first();
}
}

3
app/Providers/AppServiceProvider.php Executable file → Normal file
View file

@ -49,9 +49,8 @@ class AppServiceProvider extends ServiceProvider
public function register()
{
if ($this->app->environment() !== 'production') {
if ($this->app->environment() !== 'production' && class_exists(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class)) {
$this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
}
// ...
}
}

0
app/Providers/AuthServiceProvider.php Executable file → Normal file
View file

0
app/Providers/BroadcastServiceProvider.php Executable file → Normal file
View file

View file

@ -44,10 +44,12 @@ class DomainServiceProvider extends ServiceProvider
// Analysiere den Host der aktuellen Anfrage
$domainInfo = $domainService->parseDomain($request->getHost());
if (config('app.debug')) {
\Log::debug('DomainServiceProvider: domainInfo', [
'domainInfo' => $domainInfo,
'host' => $request->getHost()
]);
}
$userShop = null;
// Wenn es sich um eine User-Shop-Domain handelt, versuche das Shop-Objekt zu finden.

0
app/Providers/EventServiceProvider.php Executable file → Normal file
View file

0
app/Providers/RouteServiceProvider.php Executable file → Normal file
View file

0
app/Providers/YardServiceProvider.php Executable file → Normal file
View file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
<?php
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Http\Controllers\SettingController;
/**
* DHL Data Helper
*
* Central class for preparing DHL API data structures
* Prevents code duplication between DhlShipmentService and CreateShipmentJob
*/
class DhlDataHelper
{
/**
* Prepare order data for DHL API v2
*
* Structure matches official DHL API v2 createOrders endpoint:
* https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2
*
* @param ShoppingOrder $order
* @param float $weight
* @param array $options
* @param array|null $dhlConfig Optional pre-loaded config (for queue jobs)
* @return array
*/
public static function prepareOrderData(ShoppingOrder $order, float $weight, array $options = [], ?array $dhlConfig = null): array
{
\Log::info('prepareOrderData', $options);
//die daten für das versandlabel werden immer aus dem Formular genommen, damit anpassungen möglich sind
if (!isset($options['shipping_address'])) {
throw new \Exception('shipping_address is required');
}
$shippingAddress = $options['shipping_address'];
// Get DHL configuration for shipper data
if ($dhlConfig === null) {
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
}
return [
'order_id' => $order->id,
'weight_kg' => $weight,
'product_code' => $options['product_code'] ?? 'V01PAK',
'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,
// Shipper data (sender) - from admin settings
'shipper' => [
'name' => $dhlConfig['sender']['company'] ?? 'mivita care gmbh',
'name2' => $dhlConfig['sender']['name'] ?? '',
'street' => $dhlConfig['sender']['street'] ?? 'Leinfeld',
'houseNumber' => $dhlConfig['sender']['house_number'] ?? '2',
'postalCode' => $dhlConfig['sender']['postalCode'] ?? '87755',
'city' => $dhlConfig['sender']['city'] ?? 'Kirchhaslach',
'country' => $dhlConfig['sender']['country'] ?? 'DE',
'email' => $dhlConfig['sender']['email'] ?? 'versand@mivita.care',
'phone' => $dhlConfig['sender']['phone'] ?? '+49 123 456789',
],
// Consignee data (recipient) - from order
'consignee' => [
'name' => $shippingAddress['firstname'] ?? '' . ' ' . $shippingAddress['lastname'] ?? '',
'name2' => $shippingAddress['company'] ?? '',
'street' => $shippingAddress['address'] ?? '',
'houseNumber' => $shippingAddress['houseNumber'] ?? '',
'postalCode' => $shippingAddress['zipcode'] ?? '',
'city' => $shippingAddress['city'] ?? '',
'country' => $shippingAddress['country']?->code ?? 'DE',
'email' => $shippingAddress['email'] ?? '',
'phone' => $shippingAddress['phone'] ?? '',
],
// Package dimensions from options or defaults
'dimensions' => [
'length' => $options['length'] ?? 30,
'width' => $options['width'] ?? 25,
'height' => $options['height'] ?? 10,
],
// Additional services
'services' => $options['services'] ?? [],
// Custom reference for tracking
'reference' => 'Order-' . $order->id,
];
}
}

View file

@ -0,0 +1,439 @@
<?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' => []
];
// 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;
// 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 successfully', [
'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',
])->find($id);
}
/**
* Calculate order weight in kg
*
* @param ShoppingOrder $order
* @return float
*/
private function calculateOrderWeight(ShoppingOrder $order): float
{
return $order->weight / 100;
/*
// 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,
'phone' => trim($formData['shipping_phone'] ?? '')
];
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace App\Services;
use App\Models\ShoppingOrder;
use App\Http\Controllers\SettingController;
use App\Jobs\CreateShipmentJob;
use App\Services\DhlDataHelper;
use Illuminate\Support\Facades\Log;
use Exception;
/**
* 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)
*
* @param ShoppingOrder $order
* @param float $weight
* @param array $options
* @return array
*/
public function createShipment(ShoppingOrder $order, float $weight = 1.0, array $options = []): array
{
// Get DHL configuration
$settingController = new SettingController();
$dhlConfig = $settingController->getDhlConfig();
\Log::info('dhlConfig', $dhlConfig);
// Check if queue should be used
$useQueue = $dhlConfig['use_queue'] ?? false;
if ($useQueue) {
return $this->createShipmentAsync($order, $weight, $options, $dhlConfig);
} else {
return $this->createShipmentSync($order, $weight, $options, $dhlConfig);
}
}
/**
* 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
{
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
*
* @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
]);
// Create DHL client directly
$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('orderData', $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 (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
];
}
}
}

View file

@ -126,8 +126,10 @@ class DomainService
$subdomain = null;
if (count($parts) > 2) {
$subdomain = $parts[0];
if (config('app.debug')) {
\Log::debug('DomainService: Using extracted subdomain', ['subdomain' => $subdomain, 'host' => $host]);
}
}
// Determine domain type based on subdomain and host
$type = $this->determineDomainType($host, $subdomain);

View file

@ -270,6 +270,9 @@ class Payment
public static function paymentStatusSendMail(ShoppingOrder $shopping_order, $shopping_payment, $data){
$bcc = [];
$billing_email = $shopping_order->shopping_user->billing_email;
// Überprüfung der Billing-E-Mail-Adresse
if(!$billing_email){
if($data['mode'] === 'test'){
$billing_email = config('app.checkout_test_mail');
@ -277,6 +280,11 @@ class Payment
$billing_email = config('app.checkout_mail');
}
}
if(!filter_var($billing_email, FILTER_VALIDATE_EMAIL)){
\Log::channel('payment')->error("Invalid billing email at shopping_order ".$shopping_order->id, ['billing_email' => $billing_email]);
$billing_email = config('app.checkout_mail');
}
if($data['mode'] === 'test'){
$bcc[] = config('app.checkout_test_mail');
}else{

View file

@ -1,4 +1,5 @@
<?php
namespace App\Services;
use App\Models\Country;
@ -23,70 +24,76 @@ class Util
{
$uuid = (string) Str::uuid();
$e_uuid = explode("-", $uuid);
if(isset($e_uuid[0]) && $e_uuid[1]){
return $e_uuid[0]."-".$e_uuid[1];
if (isset($e_uuid[0]) && $e_uuid[1]) {
return $e_uuid[0] . "-" . $e_uuid[1];
}
return $uuid;
}
public static function formatDate(){
if(\App::getLocale() === "en"){
public static function formatDate()
{
if (\App::getLocale() === "en") {
return 'yyyy-mm-dd';
}
return 'dd.mm.yyyy';
}
public static function formatDateDB(){
if(\App::getLocale() === "en"){
public static function formatDateDB()
{
if (\App::getLocale() === "en") {
return 'Y-m-d';
}
return 'd.m.Y';
}
public static function formatDateTimeDB(){
if(\App::getLocale() === "en"){
public static function formatDateTimeDB()
{
if (\App::getLocale() === "en") {
return 'Y-m-d - H:i';
}
return 'd.m.Y - H:i';
}
public static function _format_number($value){
public static function _format_number($value)
{
return preg_replace("/[^0-9,-]/", "", $value);
}
public static function _thousands_separator(){
public static function _thousands_separator()
{
return \App::getLocale() === "en" ? ',' : '.';
}
public static function _decimal_separator(){
public static function _decimal_separator()
{
return \App::getLocale() === "en" ? '.' : ',';
}
public static function maxStrLength($str, $length = 40){
public static function maxStrLength($str, $length = 40)
{
if(strlen($str) > $length){
if (strlen($str) > $length) {
$str = substr($str, 0, $length);
//$str = substr($str, 0, strrpos($str, " "));
$str = $str." ...";
$str = $str . " ...";
}
return $str;
}
public static function reFormatNumber($value){
public static function reFormatNumber($value)
{
return (float) str_replace(',', '.', self::_format_number($value));
}
public static function formatNumber($value, $dec=2){
public static function formatNumber($value, $dec = 2)
{
$value = floatval(str_replace(',', '', $value));
return number_format($value, $dec, self::_decimal_separator(), self::_thousands_separator());
}
public static function cleanIntegerFromString($value) {
public static function cleanIntegerFromString($value)
{
// Entferne alle nicht-numerischen Zeichen außer Minus
$cleanStr = preg_replace("/[^0-9-]/", "", $value);
@ -95,15 +102,17 @@ class Util
return $number;
}
public static function cleanNumberFormat($num = 0, $dec = 2, $fullzero = false){
public static function cleanNumberFormat($num = 0, $dec = 2, $fullzero = false)
{
if($fullzero && $num == 0){
if ($fullzero && $num == 0) {
return number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator());
}
return rtrim(rtrim(number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator()),'0'), self::_decimal_separator());
return rtrim(rtrim(number_format($num, $dec, self::_decimal_separator(), self::_thousands_separator()), '0'), self::_decimal_separator());
}
public static function utf8ize( $mixed ) {
public static function utf8ize($mixed)
{
if (is_array($mixed)) {
foreach ($mixed as $key => $value) {
$mixed[$key] = self::utf8ize($value);
@ -115,14 +124,17 @@ class Util
}
public static function getPostRoute(){
public static function getPostRoute()
{
return self::$postRoute;
}
public static function setPostRoute($postRoute){
public static function setPostRoute($postRoute)
{
self::$postRoute = $postRoute;
}
public static function getUserShop(){
public static function getUserShop()
{
$shop = session('user_shop');
if (empty($shop) || !is_object($shop)) {
return null;
@ -130,17 +142,19 @@ class Util
return $shop;
}
public static function getDefaultUserShop(){
public static function getDefaultUserShop()
{
$user = \App\User::find(6);
if($user && $user->shop){
if ($user && $user->shop) {
return $user->shop;
}
return false;
}
public static function getAuthUser(){
if(\Session::has('auth_user')){
if($auth_user = \Session::get('auth_user')){
public static function getAuthUser()
{
if (\Session::has('auth_user')) {
if ($auth_user = \Session::get('auth_user')) {
return $auth_user;
}
}
@ -148,91 +162,102 @@ class Util
}
public static function getUserShopIdentifier(){
if(\Session::has('user_shop_identifier')){
if($user_shop_identifier = \Session::get('user_shop_identifier')){
public static function getUserShopIdentifier()
{
if (\Session::has('user_shop_identifier')) {
if ($user_shop_identifier = \Session::get('user_shop_identifier')) {
return $user_shop_identifier;
}
}
return false;
}
public static function getInstanceStatus(){
public static function getInstanceStatus()
{
$identifier = self::getUserShopIdentifier();
if($identifier && \Session::has('user_shop_payment') && \Session::get('user_shop_payment') === 6){
if ($identifier && \Session::has('user_shop_payment') && \Session::get('user_shop_payment') === 6) {
return OrderPaymentService::getInstanceStatus($identifier);
}
return false;
}
public static function setInstanceStatus($status, $lower = true){
public static function setInstanceStatus($status, $lower = true)
{
$identifier = self::getUserShopIdentifier();
if($identifier && \Session::has('user_shop_payment') && \Session::get('user_shop_payment') === 6){
if ($identifier && \Session::has('user_shop_payment') && \Session::get('user_shop_payment') === 6) {
OrderPaymentService::updateInstanceStatus($identifier, $status, $lower);
}
}
public static function setInstanceStatusByPayment($shopping_payment, $status, $lower = true){
if($shopping_payment->identifier){
public static function setInstanceStatusByPayment($shopping_payment, $status, $lower = true)
{
if ($shopping_payment->identifier) {
OrderPaymentService::updateInstanceStatus($shopping_payment->identifier, $status, $lower);
}
}
public static function getShoppingInstance(){
if(\Session::has('shopping_instance')){
public static function getShoppingInstance()
{
if (\Session::has('shopping_instance')) {
return \Session::get('shopping_instance');
}
return false;
}
public static function getUserHistory(){
public static function getUserHistory()
{
$auth_user = self::getAuthUser();
$user_shop_identifier = self::getUserShopIdentifier();
if($user_shop_identifier && $auth_user){
if ($user_shop_identifier && $auth_user) {
return UserHistory::whereUserId($auth_user->id)->whereIdentifier($user_shop_identifier)->get()->last();
}
return false;
}
public static function setUserHistoryValue($values = []){
if($user_history = self::getUserHistory()){
foreach ($values as $key=>$val){
public static function setUserHistoryValue($values = [])
{
if ($user_history = self::getUserHistory()) {
foreach ($values as $key => $val) {
$user_history->{$key} = $val;
}
$user_history->save();
}
}
public static function getUserHistoryValue($key){
if($user_history = self::getUserHistory()) {
public static function getUserHistoryValue($key)
{
if ($user_history = self::getUserHistory()) {
return $user_history->{$key};
}
return null;
}
public static function getUserShoppingMode(){
if($auth_user = self::getAuthUser()){
if($auth_user->isTestMode()){
public static function getUserShoppingMode()
{
if ($auth_user = self::getAuthUser()) {
if ($auth_user->isTestMode()) {
return 'test';
}
}
return config('app.mode');
}
public static function addRoute($p = []){
public static function addRoute($p = [])
{
$b = [];
if(\Session::has('user_shop')){
if($user_shop = \Session::get('user_shop')){
if (\Session::has('user_shop')) {
if ($user_shop = \Session::get('user_shop')) {
$b = ['subdomain' => $user_shop->slug];
}
}
return array_merge($p, $b);
}
public static function checkUserLandIsNot($user){
public static function checkUserLandIsNot($user)
{
if(isset($user->account->country_id)){
if (isset($user->account->country_id)) {
//ch schweiz is out
if($user->account->country_id === 6){
if ($user->account->country_id === 6) {
return false;
}
return true;
@ -241,9 +266,10 @@ class Util
}
public static function getMyMivitaShopUrl($add_url = ""){
if(\Session::has('user_shop_domain')){
$url = \Session::get('user_shop_domain').$add_url;
public static function getMyMivitaShopUrl($add_url = "")
{
if (\Session::has('user_shop_domain')) {
$url = \Session::get('user_shop_domain') . $add_url;
if (!str_starts_with($url, 'http')) {
$url = 'https://' . ltrim($url, '/');
}
@ -251,83 +277,90 @@ class Util
}
//alois sein shop
$user = \App\User::find(6);
if($user && $user->shop){
return config('app.protocol').$user->shop->slug.".".config('app.domain').config('app.tld_care').$add_url;
if ($user && $user->shop) {
return config('app.protocol') . $user->shop->slug . "." . config('app.domain') . config('app.tld_care') . $add_url;
}
}
public static function getMyMivitaPortalUrl($protocol = true){
public static function getMyMivitaPortalUrl($protocol = true)
{
$pro = $protocol ? config('app.protocol') : "";
return $pro.config('app.pre_url_portal').config('app.domain').config('app.tld_care');
return $pro . config('app.pre_url_portal') . config('app.domain') . config('app.tld_care');
}
public static function getMyMivitaUrl($protocol = true){
public static function getMyMivitaUrl($protocol = true)
{
$pro = $protocol ? config('app.protocol') : "";
return $pro.config('app.pre_url_crm').config('app.domain').config('app.tld_care');
return $pro . config('app.pre_url_crm') . config('app.domain') . config('app.tld_care');
}
public static function getUserPaymentFor($instance = 'shopping'){
if(Yard::instance($instance)->getYardExtra('user_shop_payment')){
public static function getUserPaymentFor($instance = 'shopping')
{
if (Yard::instance($instance)->getYardExtra('user_shop_payment')) {
return Yard::instance($instance)->getYardExtra('user_shop_payment');
}
if(\Session::has('user_shop_payment')){
if (\Session::has('user_shop_payment')) {
return \Session::get('user_shop_payment');
}
return null;
}
public static function getUserShopBackUrl($reference = ""){
public static function getUserShopBackUrl($reference = "")
{
if(\Session::has('user_shop')){
if(\Session::has('user_shop_domain')){
if (\Session::has('user_shop')) {
if (\Session::has('user_shop_domain')) {
return \Session::get('user_shop_domain');
}
if($user_shop = \Session::get('user_shop')){
return config('app.protocol').$user_shop->slug.".".config('app.domain').config('app.tld_care')."/back/to/shop/".$reference;
if ($user_shop = \Session::get('user_shop')) {
return config('app.protocol') . $user_shop->slug . "." . config('app.domain') . config('app.tld_care') . "/back/to/shop/" . $reference;
}
}
return config('app.protocol').config('app.domain').config('app.tld_care');
return config('app.protocol') . config('app.domain') . config('app.tld_care');
}
public static function getUserCardBackUrl($uri, $instance = 'shopping'){
public static function getUserCardBackUrl($uri, $instance = 'shopping')
{
if(\Session::has('user_shop')){
if(\Session::has('user_shop_domain')){
if(\Session::has('back_link')){
if (\Session::has('user_shop')) {
if (\Session::has('user_shop_domain')) {
if (\Session::has('back_link')) {
return \Session::get('back_link');
}
if(self::getUserPaymentFor($instance) === 3){
return \Session::get('user_shop_domain')."/user/membership";
if (self::getUserPaymentFor($instance) === 3) {
return \Session::get('user_shop_domain') . "/user/membership";
}
if(self::getUserPaymentFor($instance) === 2){
return \Session::get('user_shop_domain')."/user/orders";
if (self::getUserPaymentFor($instance) === 2) {
return \Session::get('user_shop_domain') . "/user/orders";
}
return \Session::get('user_shop_domain');
}
if($user_shop = \Session::get('user_shop')){
return config('app.protocol').$user_shop->slug.".".config('app.domain').config('app.tld_care').$uri;
if ($user_shop = \Session::get('user_shop')) {
return config('app.protocol') . $user_shop->slug . "." . config('app.domain') . config('app.tld_care') . $uri;
}
}
return config('app.protocol').config('app.domain').config('app.tld_care');
return config('app.protocol') . config('app.domain') . config('app.tld_care');
}
public static function isMivitaShop(){
if(Request::getHost() === 'checkout.'.config('app.domain').config('app.tld_care')){
if($user_shop = \Session::get('user_shop')){
if($user_shop->slug === 'aloevera' || $user_shop->slug === 'naturcosmetic'){
public static function isMivitaShop()
{
if (Request::getHost() === 'checkout.' . config('app.domain') . config('app.tld_care')) {
if ($user_shop = \Session::get('user_shop')) {
if ($user_shop->slug === 'aloevera' || $user_shop->slug === 'naturcosmetic') {
return true;
}
}
}
if(Request::getHost() === 'naturcosmetic.'.config('app.domain').config('app.tld_care')){
if (Request::getHost() === 'naturcosmetic.' . config('app.domain') . config('app.tld_care')) {
return true;
}
return \Config::get('app.url') === config('app.domain').config('app.tld_shop');
return \Config::get('app.url') === config('app.domain') . config('app.tld_shop');
}
public static function isTestSystem($dev = false){
if(\Config::get('app.tld_care') === '.test' || \Config::get('app.tld_shop') === '.lshop'){
if($dev && config('app.debug') !== true){
public static function isTestSystem($dev = false)
{
if (\Config::get('app.tld_care') === '.test' || \Config::get('app.tld_shop') === '.lshop') {
if ($dev && config('app.debug') !== true) {
return false;
}
return true;
@ -350,16 +383,53 @@ class Util
public static function sanitize($string, $force_lowercase = true, $anal = false, $substr = false)
{
$strip = array("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]",
"}", "\\", "|", ";", ":", "\"", "'", "&#8216;", "&#8217;", "&#8220;", "&#8221;", "&#8211;", "&#8212;",
"—", "–", ",", "<", ".", ">", "/", "?");
$strip = array(
"~",
"`",
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"_",
"=",
"+",
"[",
"{",
"]",
"}",
"\\",
"|",
";",
":",
"\"",
"'",
"&#8216;",
"&#8217;",
"&#8220;",
"&#8221;",
"&#8211;",
"&#8212;",
"—",
"–",
",",
"<",
".",
">",
"/",
"?"
);
$clean = trim(str_replace($strip, "", strip_tags($string)));
$clean = preg_replace('/\s+/', "_", $clean);
$clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean ;
if($substr){
$clean = (strlen($clean) > 20) ? substr($clean,-20) : $clean;
$clean = ($anal) ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean;
if ($substr) {
$clean = (strlen($clean) > 20) ? substr($clean, -20) : $clean;
}
return ($force_lowercase) ?
(function_exists('mb_strtolower')) ?
@ -367,5 +437,4 @@ class Util
strtolower($clean) :
$clean;
}
}

View file

@ -120,6 +120,8 @@ use Laravel\Passport\HasApiTokens;
* @method static \Illuminate\Database\Eloquent\Builder|User wherePreSponsor($value)
* @property \Illuminate\Support\Carbon|null $pre_deleted_at
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePreDeletedAt($value)
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\UserBusiness> $userBusiness
* @property-read int|null $user_business_count
* @mixin \Eloquent
*/
class User extends Authenticatable

View file

@ -130,61 +130,3 @@ if (! function_exists('legal_url')) {
}
}
}
if (! function_exists('main_asset')) {
/**
* Generate an asset URL using the main domain to avoid CORS issues.
* This ensures all assets are loaded from the main domain regardless of subdomain.
*/
function main_asset($path)
{
// Entferne führende Slashes
$path = ltrim($path, '/');
// Baue die Hauptdomain-URL
$protocol = config('app.protocol', 'https://');
$domain = config('app.domain', 'mivita');
$tld = config('app.tld_care', '.care');
return $protocol . $domain . $tld . '/' . $path;
}
}
if (! function_exists('cors_asset')) {
/**
* Alias for main_asset for backward compatibility and clarity.
*/
function cors_asset($path)
{
return main_asset($path);
}
}
if (!function_exists('route')) {
/**
* Route auf Hauptdomain generieren
*/
function route($name, $parameters = [], $absolute = true)
{
$url = route($name, $parameters, $absolute);
// Ersetze Subdomain mit Hauptdomain
if (request()->hasHeader('X-Subdomain')) {
$currentHost = request()->getHost();
$url = str_replace($currentHost, config('app.domain').config('app.tld_care'), $url);
}
return $url;
}
}
if (!function_exists('asset_route')) {
/**
* Asset/Storage Route immer auf Hauptdomain
*/
function asset_route($name, $parameters = [])
{
return 'https://' . config('app.domain') . config('app.tld_care') . route($name, $parameters, false);
}
}

View file

@ -1,15 +1,4 @@
<?php return array (
'alban/laravel-collective-spatie-html-parser' =>
array (
'aliases' =>
array (
'Form' => 'Alban\\LaravelCollectiveSpatieHtmlParser\\FormFacade',
),
'providers' =>
array (
0 => 'Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider',
),
),
'barryvdh/laravel-debugbar' =>
array (
'aliases' =>

View file

@ -23,39 +23,39 @@
19 => 'Illuminate\\Translation\\TranslationServiceProvider',
20 => 'Illuminate\\Validation\\ValidationServiceProvider',
21 => 'Illuminate\\View\\ViewServiceProvider',
22 => 'Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider',
23 => 'Barryvdh\\Debugbar\\ServiceProvider',
24 => 'Barryvdh\\DomPDF\\ServiceProvider',
25 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
26 => 'Cviebrock\\EloquentSluggable\\ServiceProvider',
27 => 'Jenssegers\\Date\\DateServiceProvider',
28 => 'JoeDixon\\Translation\\TranslationServiceProvider',
29 => 'JoeDixon\\Translation\\TranslationBindingsServiceProvider',
30 => 'Laracasts\\Flash\\FlashServiceProvider',
31 => 'Illuminate\\Database\\Eloquent\\LegacyFactoryServiceProvider',
32 => 'Laravel\\Passport\\PassportServiceProvider',
33 => 'Laravel\\Sail\\SailServiceProvider',
34 => 'Laravel\\Tinker\\TinkerServiceProvider',
35 => 'Laravel\\Ui\\UiServiceProvider',
36 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
37 => 'Carbon\\Laravel\\ServiceProvider',
38 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
39 => 'Termwind\\Laravel\\TermwindServiceProvider',
40 => 'Pest\\Laravel\\PestServiceProvider',
41 => 'Spatie\\Html\\HtmlServiceProvider',
42 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
43 => 'Yajra\\DataTables\\DataTablesServiceProvider',
44 => 'Laravel\\Tinker\\TinkerServiceProvider',
22 => 'Barryvdh\\Debugbar\\ServiceProvider',
23 => 'Barryvdh\\DomPDF\\ServiceProvider',
24 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
25 => 'Cviebrock\\EloquentSluggable\\ServiceProvider',
26 => 'Jenssegers\\Date\\DateServiceProvider',
27 => 'JoeDixon\\Translation\\TranslationServiceProvider',
28 => 'JoeDixon\\Translation\\TranslationBindingsServiceProvider',
29 => 'Laracasts\\Flash\\FlashServiceProvider',
30 => 'Illuminate\\Database\\Eloquent\\LegacyFactoryServiceProvider',
31 => 'Laravel\\Passport\\PassportServiceProvider',
32 => 'Laravel\\Sail\\SailServiceProvider',
33 => 'Laravel\\Tinker\\TinkerServiceProvider',
34 => 'Laravel\\Ui\\UiServiceProvider',
35 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
36 => 'Carbon\\Laravel\\ServiceProvider',
37 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
38 => 'Termwind\\Laravel\\TermwindServiceProvider',
39 => 'Pest\\Laravel\\PestServiceProvider',
40 => 'Spatie\\Html\\HtmlServiceProvider',
41 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
42 => 'Yajra\\DataTables\\DataTablesServiceProvider',
43 => 'Laravel\\Tinker\\TinkerServiceProvider',
44 => 'Acme\\Dhl\\DhlServiceProvider',
45 => 'App\\Providers\\AppServiceProvider',
46 => 'App\\Providers\\AuthServiceProvider',
47 => 'App\\Providers\\EventServiceProvider',
48 => 'App\\Providers\\DomainServiceProvider',
49 => 'App\\Providers\\RouteServiceProvider',
50 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
51 => 'Jenssegers\\Date\\DateServiceProvider',
52 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
53 => 'Yajra\\DataTables\\DataTablesServiceProvider',
54 => 'App\\Providers\\YardServiceProvider',
50 => 'Jenssegers\\Date\\DateServiceProvider',
51 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
52 => 'Yajra\\DataTables\\DataTablesServiceProvider',
53 => 'App\\Providers\\YardServiceProvider',
54 => 'Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider',
),
'eager' =>
array (
@ -69,24 +69,24 @@
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
8 => 'Illuminate\\Session\\SessionServiceProvider',
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider',
11 => 'Barryvdh\\Debugbar\\ServiceProvider',
12 => 'Barryvdh\\DomPDF\\ServiceProvider',
13 => 'Cviebrock\\EloquentSluggable\\ServiceProvider',
14 => 'Jenssegers\\Date\\DateServiceProvider',
15 => 'JoeDixon\\Translation\\TranslationServiceProvider',
16 => 'Laracasts\\Flash\\FlashServiceProvider',
17 => 'Illuminate\\Database\\Eloquent\\LegacyFactoryServiceProvider',
18 => 'Laravel\\Passport\\PassportServiceProvider',
19 => 'Laravel\\Ui\\UiServiceProvider',
20 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
21 => 'Carbon\\Laravel\\ServiceProvider',
22 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
23 => 'Termwind\\Laravel\\TermwindServiceProvider',
24 => 'Pest\\Laravel\\PestServiceProvider',
25 => 'Spatie\\Html\\HtmlServiceProvider',
26 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
27 => 'Yajra\\DataTables\\DataTablesServiceProvider',
10 => 'Barryvdh\\Debugbar\\ServiceProvider',
11 => 'Barryvdh\\DomPDF\\ServiceProvider',
12 => 'Cviebrock\\EloquentSluggable\\ServiceProvider',
13 => 'Jenssegers\\Date\\DateServiceProvider',
14 => 'JoeDixon\\Translation\\TranslationServiceProvider',
15 => 'Laracasts\\Flash\\FlashServiceProvider',
16 => 'Illuminate\\Database\\Eloquent\\LegacyFactoryServiceProvider',
17 => 'Laravel\\Passport\\PassportServiceProvider',
18 => 'Laravel\\Ui\\UiServiceProvider',
19 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
20 => 'Carbon\\Laravel\\ServiceProvider',
21 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
22 => 'Termwind\\Laravel\\TermwindServiceProvider',
23 => 'Pest\\Laravel\\PestServiceProvider',
24 => 'Spatie\\Html\\HtmlServiceProvider',
25 => 'Spatie\\LaravelIgnition\\IgnitionServiceProvider',
26 => 'Yajra\\DataTables\\DataTablesServiceProvider',
27 => 'Acme\\Dhl\\DhlServiceProvider',
28 => 'App\\Providers\\AppServiceProvider',
29 => 'App\\Providers\\AuthServiceProvider',
30 => 'App\\Providers\\EventServiceProvider',
@ -96,6 +96,7 @@
34 => 'Maatwebsite\\Excel\\ExcelServiceProvider',
35 => 'Yajra\\DataTables\\DataTablesServiceProvider',
36 => 'App\\Providers\\YardServiceProvider',
37 => 'Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider',
),
'deferred' =>
array (

View file

@ -4,15 +4,15 @@
"keywords": ["framework", "laravel"],
"license": "MIT",
"type": "project",
"require": {
"php": "^8.2",
"alban/laravel-collective-spatie-html-parser": "^1.1.9",
"barryvdh/laravel-dompdf": "^2.2",
"cocur/slugify": "^4.5",
"cviebrock/eloquent-sluggable": "^11.0",
"doctrine/dbal": "^3.6.0|^4.0",
"guzzlehttp/guzzle": "^7.4",
"intervention/image": "^3.0",
"intervention/image": "^3",
"jenssegers/date": "^4.0",
"joedixon/laravel-translation": "2.x-dev",
"laracasts/flash": "^3.2",
@ -24,6 +24,7 @@
"maatwebsite/excel": "^3.1",
"setasign/fpdf": "^1.8.6",
"setasign/fpdi": "^2.6",
"spatie/laravel-html": "^3.7",
"wearepixel/laravel-google-shopping-feed": "^4.0",
"yajra/laravel-datatables-oracle": "^11.0"
},
@ -62,6 +63,8 @@
"psr-4": {
"App\\": "app/",
"Gloudemans\\Shoppingcart\\": "packages/digital-bird/shoppingcart/src",
"Alban\\LaravelCollectiveSpatieHtmlParser\\": "packages/laravel-collective-spatie-html-parser/src",
"Acme\\Dhl\\": "packages/acme-laravel-dhl/src",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
@ -78,10 +81,7 @@
"@php artisan vendor:publish --force --tag=livewire:assets --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"php artisan ide-helper:models"
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
@ -95,6 +95,10 @@
]
},
"repositories": [
{
"type": "path",
"url": "./packages/acme-laravel-dhl"
},
{
"type": "vcs",
"url": "https://github.com/bjhijmans/laravel-translation.git"
@ -102,4 +106,5 @@
],
"minimum-stability": "dev",
"prefer-stable": true
}

845
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -52,7 +52,8 @@ return [
|
*/
'mode' => env('APP_MODE', 'live'),
'url' => env('APP_URL', 'https://mivita'),
'url' => env('APP_URL', 'https://mivita.care'),
'url_crm' => env('APP_URL_CRM', 'https://my.mivita.care'),
'domain' => env('APP_DOMAIN', 'mivita'),
'tld_care' => env('APP_TLD_CARE', '.local'),
'tld_shop' => env('APP_TLD_SHOP', '.lshop'),
@ -185,6 +186,10 @@ return [
*/
Laravel\Tinker\TinkerServiceProvider::class,
/*
* DHL Package Service Provider...
*/
Acme\Dhl\DhlServiceProvider::class,
/*
* Application Service Providers...
@ -195,11 +200,11 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\DomainServiceProvider::class,
App\Providers\RouteServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Jenssegers\Date\DateServiceProvider::class,
Maatwebsite\Excel\ExcelServiceProvider::class,
Yajra\DataTables\DataTablesServiceProvider::class,
App\Providers\YardServiceProvider::class,
Alban\LaravelCollectiveSpatieHtmlParser\ServiceProvider::class,
],
/*
@ -257,6 +262,9 @@ return [
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
'DataTables' => Yajra\DataTables\Facades\DataTables::class,
'Yard' => App\Services\Facade\Yard::class,
'DHL' => Acme\Dhl\Facades\DHL::class,
'Form' => Alban\LaravelCollectiveSpatieHtmlParser\FormFacade::class,
'Html' => Alban\LaravelCollectiveSpatieHtmlParser\HtmlFacade::class,
],
];

108
config/dhl.php Normal file
View file

@ -0,0 +1,108 @@
<?php
/*
|--------------------------------------------------------------------------
| DHL Package Configuration (acme/laravel-dhl)
|--------------------------------------------------------------------------
|
| Configuration for the new DHL Laravel package with direct API integration
|
| Settings can be managed via Admin panel at /admin/settings
| Admin settings override these .env values at runtime
|
*/
return [
/*
|--------------------------------------------------------------------------
| DHL API Settings
|--------------------------------------------------------------------------
*/
'base_url' => env('DHL_BASE_URL', 'https://api-eu.dhl.com'),
'api_key' => env('DHL_API_KEY'),
'username' => env('DHL_USERNAME'),
'password' => env('DHL_PASSWORD'),
'billing_number' => env('DHL_BILLING_NUMBER'),
/*
|--------------------------------------------------------------------------
| Default Product Settings
|--------------------------------------------------------------------------
*/
'default_product' => env('DHL_PRODUCT', 'V01PAK'),
'label_format' => env('DHL_LABEL_FORMAT', 'PDF'),
'print_format' => env('DHL_PRINT_FORMAT', 'A4'),
'retoure_print_format' => env('DHL_RETOURE_PRINT_FORMAT', 'A4'),
'profile' => env('DHL_PROFILE', 'STANDARD_GRUPPENPROFIL'),
/*
|--------------------------------------------------------------------------
| Queue Settings
|--------------------------------------------------------------------------
*/
'use_queue' => env('DHL_USE_QUEUE', false),
/*
|--------------------------------------------------------------------------
| Webhook Configuration
|--------------------------------------------------------------------------
*/
'webhook' => [
'enabled' => env('DHL_WEBHOOK_ENABLED', false),
'secret' => env('DHL_WEBHOOK_SECRET'),
'route' => env('DHL_WEBHOOK_ROUTE', 'dhl/webhooks/tracking')
],
/*
|--------------------------------------------------------------------------
| Sender Address
|--------------------------------------------------------------------------
*/
'sender' => [
'company' => env('DHL_SENDER_COMPANY', 'mivita care gmbh'),
'name' => env('DHL_SENDER_NAME', ''),
'street' => env('DHL_SENDER_STREET', 'Leinfeld'),
'houseNumber' => env('DHL_SENDER_STREET_NUMBER', '2'),
'postalCode' => env('DHL_SENDER_POSTAL_CODE', '87755'),
'city' => env('DHL_SENDER_CITY', 'Kirchhaslach'),
'country' => env('DHL_SENDER_COUNTRY', 'DE'),
'email' => env('DHL_SENDER_EMAIL', 'versand@mivita.care'),
'phone' => env('DHL_SENDER_PHONE', '+49 123 456789'),
],
/*
|--------------------------------------------------------------------------
| Account Numbers
|--------------------------------------------------------------------------
*/
'account_numbers' => [
'default' => env('DHL_ACCOUNT_NUMBER_DEFAULT', '63144073550101'),
'V01PAK' => env('DHL_ACCOUNT_NUMBER_V01PAK', '63144073550101'), // DHL Paket National
'V62WP' => env('DHL_ACCOUNT_NUMBER_V62WP', '63144073556201'), // Warenpost National
'V53PAK' => env('DHL_ACCOUNT_NUMBER_V53PAK', '63144073555301'), // DHL Paket International
'V07PAK' => env('DHL_ACCOUNT_NUMBER_V07PAK', '63144073550701'), // DHL Retoure Online
],
/*
|--------------------------------------------------------------------------
| Logging Settings
|--------------------------------------------------------------------------
*/
'logging' => [
'enabled' => env('DHL_LOGGING_ENABLED', true),
'level' => env('DHL_LOGGING_LEVEL', 'info'),
'channel' => env('DHL_LOGGING_CHANNEL', 'single'),
],
/*
|--------------------------------------------------------------------------
| Legacy Settings (for compatibility)
|--------------------------------------------------------------------------
*/
'legacy' => [
'api_type' => env('DHL_API_TYPE', 'developer'),
'api_secret' => env('DHL_API_SECRET'),
'sandbox' => env('DHL_SANDBOX', true),
'test_mode' => env('DHL_TEST_MODE', true),
]
];

View file

@ -133,5 +133,16 @@ return [
'mivita',
'shop',
'myaloe',
'my',
'in',
'portal',
'checkout',
'login',
'logout',
'register',
'forgot',
'reset',
'verify',
'confirm',
]
];

View file

@ -13,7 +13,7 @@ return [
|
*/
'default' => env('QUEUE_DRIVER', 'sync'),
'default' => env('QUEUE_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------

View file

@ -0,0 +1,109 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('dhl_shipments', function (Blueprint $table) {
$table->id();
// Relationship zu ShoppingOrder
$table->unsignedInteger('shopping_order_id'); // int(10) unsigned to match shopping_orders.id
$table->foreign('shopping_order_id')->references('id')->on('shopping_orders')->onDelete('cascade');
// DHL Shipment Daten
$table->string('shipment_number')->nullable(); // DHL Sendungsnummer
$table->string('tracking_number')->nullable(); // DHL Tracking-Nummer (kann identisch sein)
// Sendungstyp: 'outbound' (regulär) oder 'return' (Retoure)
$table->enum('type', ['outbound', 'return'])->default('outbound');
// Für Retouren: Verweis auf ursprüngliche Sendung
$table->unsignedBigInteger('related_shipment_id')->nullable(); // id() uses bigInteger
$table->foreign('related_shipment_id')->references('id')->on('dhl_shipments')->onDelete('set null');
// Paketdaten
$table->decimal('weight', 8, 2)->default(1.0); // in kg
$table->integer('length')->nullable(); // in cm
$table->integer('width')->nullable(); // in cm
$table->integer('height')->nullable(); // in cm
// DHL Service-Optionen
$table->string('product_code')->default('V01PAK'); // V01PAK = DHL Paket
$table->json('services')->nullable(); // Zusätzliche Services (Premium, etc.)
// Labels und Dokumente
$table->string('label_path')->nullable(); // Pfad zum generierten Label
$table->string('label_format')->default('PDF'); // PDF oder ZPL
$table->boolean('label_printed')->default(false);
// Status-Management
$table->enum('status', [
'created', // Sendung erstellt, noch nicht bei DHL
'submitted', // An DHL übertragen
'in_transit', // Unterwegs
'delivered', // Zugestellt
'returned', // Zurückgeschickt
'cancelled', // Storniert
'failed' // Fehler bei Erstellung/Übertragung
])->default('created');
// Tracking-Status (von DHL API)
$table->string('tracking_status')->nullable();
$table->text('tracking_details')->nullable(); // JSON für detaillierte Tracking-Info
$table->timestamp('last_tracked_at')->nullable();
// Empfängeradresse (Kopie für Archivierung)
$table->string('recipient_name');
$table->string('recipient_company')->nullable();
$table->string('recipient_street');
$table->string('recipient_street_number');
$table->string('recipient_postal_code');
$table->string('recipient_city');
$table->string('recipient_state')->nullable();
$table->string('recipient_country', 2)->default('DE');
$table->string('recipient_email')->nullable();
$table->string('recipient_phone')->nullable();
// API Response Data (für Debugging und Audit)
$table->json('api_request_data')->nullable(); // Gesendete Daten
$table->json('api_response_data')->nullable(); // Empfangene Antwort
$table->text('api_errors')->nullable(); // Fehlermeldungen
// Kosten und Abrechnung
$table->decimal('shipping_cost', 10, 2)->nullable(); // Versandkosten
$table->string('currency', 3)->default('EUR');
// Zusätzliche Metadaten
$table->text('notes')->nullable(); // Interne Notizen
$table->json('metadata')->nullable(); // Flexible zusätzliche Daten
// Timestamps
$table->timestamp('shipped_at')->nullable(); // Wann wurde versendet
$table->timestamp('delivered_at')->nullable(); // Wann zugestellt
$table->timestamps();
// Indizes für Performance
$table->index('shopping_order_id');
$table->index('shipment_number');
$table->index('tracking_number');
$table->index(['type', 'status']);
$table->index('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('dhl_shipments');
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('dhl_package_shipments', function (Blueprint $table) {
$table->string('routing_code')->nullable()->after('dhl_shipment_no');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('dhl_package_shipments', function (Blueprint $table) {
$table->dropColumn('routing_code');
});
}
};

View file

@ -0,0 +1,151 @@
# DHL Modul - Aktualisierung auf Paket-Ansatz
## Überarbeitung: Von SDK zu eigenständigem Laravel-Paket
**Datum**: $(date '+%Y-%m-%d')
**Grund**: Das christoph-schaeffer/dhl-business-shipping SDK ist veraltet und macht Probleme mit DHL-Login
## Neue Architektur: packages/acme-laravel-dhl
### ✅ Durchgeführte Überarbeitungen
#### 1. Code Refactoring für bessere Lesbarkeit
- **Services vollständig überarbeitet**:
- `ShippingService` - Versandlabel-Erstellung mit klarer Struktur
- `TrackingService` - Tracking-Status mit Bulk-Updates
- `ReturnsService` - Retourenlabel-Management
- `DhlClient` - HTTP-Client mit umfassendem Error-Handling
- **Verbesserungen**:
- Vollständige PHPDoc-Dokumentation
- Typisierung aller Parameter und Rückgabewerte
- Private Methoden für bessere Code-Organisation
- Validierung aller Eingabedaten
- Exception-Handling mit aussagekräftigen Fehlermeldungen
#### 2. Datenbankschema an optimierten Plan angepasst
- **Vereinfachtes Schema** entsprechend PLAN-OPTIMIERT.md:
- Eine zentrale `dhl_shipments` Tabelle
- `type` Spalte für Outbound/Return-Unterscheidung
- `related_shipment_id` für Retour-Verknüpfung
- Tracking-Status direkt in Haupttabelle
- **Migration erstellt**:
- `/database/migrations/2025_01_01_000000_create_dhl_shipments_table.php`
- `/database/migrations/2025_01_01_000200_create_dhl_tracking_events_table.php`
- **Neue Models**:
- `DhlShipment` - Hauptmodel mit Relationships und Scopes
- `DhlTrackingEvent` - Tracking-Events für Phase 2
#### 3. Error Handling und Validation implementiert
- **DhlClient mit robustem Error-Handling**:
- HTTP-Status-Code spezifische Exceptions
- Retry-Mechanismus (3 Versuche)
- Timeout-Handling (30 Sekunden)
- User-Agent für API-Identifikation
- **Service-Validierung**:
- Eingabedaten-Validierung vor API-Aufrufen
- Konfigurationsprüfung (Billing Number, etc.)
- Aussagekräftige Exception-Messages
### 🗂️ Neue Paket-Struktur
```
packages/acme-laravel-dhl/
├── composer.json # Laravel-Paket Konfiguration
├── config/dhl.php # DHL API Konfiguration
├── database/migrations/ # Datenbank-Struktur
│ ├── 2025_01_01_000000_create_dhl_shipments_table.php
│ └── 2025_01_01_000200_create_dhl_tracking_events_table.php
├── src/
│ ├── DhlServiceProvider.php # Laravel Service Provider
│ ├── DhlManager.php # Hauptmanager-Klasse
│ ├── Facades/DHL.php # Laravel Facade
│ ├── Models/
│ │ ├── DhlShipment.php # Zentrales Shipment-Model
│ │ └── DhlTrackingEvent.php # Tracking-Events
│ ├── Services/
│ │ ├── ShippingService.php # Versandlabel-Service
│ │ ├── TrackingService.php # Tracking-Service
│ │ └── ReturnsService.php # Retour-Service
│ ├── Support/
│ │ └── DhlClient.php # HTTP-API-Client
│ └── Http/Controllers/ # Für spätere Web-Integration
└── routes/api.php # API-Routen für Webhooks
```
### 📋 Aktualisierter Implementierungsplan
#### Phase 1: Paket-Integration (2 Schritte)
1. **Paket-Registrierung im Hauptprojekt**
- composer.json Repositories-Eintrag
- Service Provider Registration
- Konfiguration publizieren
2. **API-Credentials Setup**
- .env Variablen konfigurieren
- Test-Verbindung zur DHL API
#### Phase 2: Admin-Integration (4 Schritte)
3. **Admin-Controller erstellen**
- DhlShipmentController für Backend
- Integration in bestehende Order-Verwaltung
4. **Blade-Views für DHL Cockpit**
- Sendungsübersicht mit DataTables
- Label-Download und -Druck
5. **Queue-Jobs für Async-Processing**
- CreateShipmentJob
- CancelShipmentJob
- CreateReturnLabelJob
6. **Tracking-Automation**
- UpdateTrackingStatusJob
- Artisan Command für Scheduler
#### Phase 3: Testing & Finalisierung (2 Schritte)
7. **Unit/Feature Tests**
- Service-Tests mit Mocked API
- Controller-Tests
- Database-Tests
8. **Documentation & Polish**
- Benutzerhandbuch
- API-Dokumentation
- Performance-Optimierung
### ⚠️ Breaking Changes zum alten Ansatz
1. **Namespace geändert**: `App\Services\DhlApiService``Acme\Dhl\Services\*`
2. **Datenbank**: `dhl_shipments` statt mehrerer separater Tabellen
3. **Model**: `DhlShipment` statt `DhlShipment` (ähnlich, aber neue Struktur)
4. **API-Integration**: Direkter HTTP-Client statt SDK-Wrapper
### 🔧 Migration vom alten System
Falls bereits bestehende DHL-Integration vorhanden:
1. Bestehende Daten nach `dhl_shipments` migrieren
2. Controller-Aufrufe auf neue Services umstellen
3. View-Integrationen aktualisieren
### 📦 Vorteile des Paket-Ansatzes
- **Unabhängigkeit**: Keine Abhängigkeit von veralteten SDKs
- **Wartbarkeit**: Sauberer, dokumentierter Code
- **Flexibilität**: Direkter API-Zugang ermöglicht alle DHL-Features
- **Testbarkeit**: Vollständig mockbare Services
- **Laravel-Integration**: Native Laravel-Patterns und Features
- **Wiederverwendbarkeit**: Als eigenständiges Paket nutzbar
### 🎯 Nächste Schritte
1. **Paket im Hauptprojekt registrieren**
2. **DHL API-Credentials konfigurieren**
3. **Erste Testlabel erstellen**
4. **Admin-Interface implementieren**
5. **Bestehende Order-Integration anpassen**
Das überarbeitete System ist nun deutlich stabiler, wartbarer und zukunftssicher.

View file

@ -0,0 +1,33 @@
# Optimierungen am DHL-Modul
## Code-Verbesserungen
- Erweiterte Logging in allen Services und DhlClient.
- Robuste Payload-Extraktion mit data_get().
- Exponential Backoff für Rate-Limits in DhlClient.
## Behebene Fehler
- Namenskonflikt: Shipment zu DhlShipment geändert.
- Erweiterte Status-Updates in Webhook und TrackingService (mehr Codes gemappt).
- Validierung in cancelLabel: Prüft Existenz und canCancel().
## Optionale Queues
- Neu in config: 'use_queue' => false (Standard: synchron).
- In Services: Wenn true, dispatch Job (z.B. SyncTrackingJob); sonst synchron.
- Für Entwicklung/Erstrollout: Synchron. Bei höherer Last: Aktivieren für Asynchronität.
## Weitere Dokumentation
- Aktualisierte README.md und PAKET-INSTALLATION.md mit Queue-Infos.
## Weitere Fixes
- DhlClient: Sleep zu usleep geändert mit Cap auf 10 Sekunden.
- ShippingService: HouseNumber required for DHL!, Retry für Storage::put.
- TrackingService: Transaction um firstOrCreate.
- Migration: Kommentar zur anonymen Klasse.
- Status-Mappings: Zentral in DhlShipment, referenziert in TrackingService.
- DhlManager: Descriptive Variablen, Typ-Hints.
- Storage-Retry: 3 Versuche mit Logging in ShippingService.

View file

@ -0,0 +1,257 @@
# DHL Paket Installation & Setup
## 1. Paket im Hauptprojekt registrieren
### composer.json erweitern
```json
{
"repositories": [
{
"type": "path",
"url": "./packages/acme-laravel-dhl"
}
],
"require": {
"acme/laravel-dhl": "*"
}
}
```
### Installation
```bash
composer update acme/laravel-dhl
```
## 2. Service Provider registrieren
### config/app.php (falls Auto-Discovery nicht funktioniert)
```php
'providers' => [
// ...
Acme\Dhl\DhlServiceProvider::class,
],
'aliases' => [
// ...
'DHL' => Acme\Dhl\Facades\DHL::class,
],
```
## 3. Konfiguration publizieren
```bash
# Konfigurationsdatei publizieren
php artisan vendor:publish --provider="Acme\Dhl\DhlServiceProvider" --tag="config"
# Migrations ausführen
php artisan migrate
```
## 4. Umgebungsvariablen konfigurieren
### .env erweitern
```env
# DHL API Configuration
DHL_BASE_URL=https://api-eu.dhl.com
DHL_API_KEY=your_api_key_here
DHL_USERNAME=your_username
DHL_PASSWORD=your_password
DHL_BILLING_NUMBER=your_billing_number
# DHL Default Settings
DHL_PRODUCT=V01PAK
DHL_LABEL_FORMAT=PDF
DHL_PRINT_FORMAT=A4
DHL_RETOURE_PRINT_FORMAT=A4
DHL_PROFILE=STANDARD_GRUPPENPROFIL
# DHL Webhook (optional)
DHL_WEBHOOK_ENABLED=false
DHL_WEBHOOK_SECRET=your_webhook_secret
DHL_WEBHOOK_ROUTE=dhl/webhooks/tracking
# DHL Queue Settings (optional)
DHL_USE_QUEUE=false
```
## 5. Test-Verbindung prüfen
### Artisan Command (zu erstellen)
```bash
php artisan dhl:test-connection
```
### Oder via Tinker
```php
php artisan tinker
// Service-Container Test
app(Acme\Dhl\Services\ShippingService::class);
// Model Test
use Acme\Dhl\Models\DhlShipment;
DhlShipment::query()->count();
// API-Test (vereinfacht)
// $client = app(Acme\Dhl\Support\DhlClient::class);
// $client->testConnection();
```
## 6. Integration in bestehende Models
### ShoppingOrder Model erweitern
```php
use Acme\Dhl\Models\DhlShipment;
class ShoppingOrder extends Model
{
/**
* DHL Sendungen für diese Bestellung
*/
public function dhlShipments(): HasMany
{
return $this->hasMany(DhlShipment::class, 'order_id');
}
/**
* Ausgehende DHL Sendungen
*/
public function dhlOutboundShipments(): HasMany
{
return $this->dhlShipments()->outbound();
}
/**
* DHL Retouren
*/
public function dhlReturns(): HasMany
{
return $this->dhlShipments()->returns();
}
/**
* Hat DHL Sendungen
*/
public function hasDhlShipments(): bool
{
return $this->dhlShipments()->exists();
}
/**
* Neueste DHL Sendung
*/
public function latestDhlShipment(): ?DhlShipment
{
return $this->dhlShipments()
->latest()
->first();
}
}
```
## 7. Verwendung in Controllern
### Beispiel Controller-Integration
```php
use Acme\Dhl\Services\ShippingService;
use Acme\Dhl\Services\TrackingService;
use Acme\Dhl\Services\ReturnsService;
class OrderController extends Controller
{
public function createDhlLabel(
ShoppingOrder $order,
ShippingService $shippingService
) {
try {
$result = $shippingService->createLabel([
'order_id' => $order->id,
'shipper' => [
'name' => 'Ihre Firma',
'street' => 'Ihre Straße 123',
'postalCode' => '12345',
'city' => 'Ihre Stadt',
'country' => 'DE'
],
'consignee' => [
'name' => $order->shipping_name,
'street' => $order->shipping_street,
'postalCode' => $order->shipping_zip,
'city' => $order->shipping_city,
'country' => $order->shipping_country
],
'weight_kg' => $order->total_weight ?? 1.0
]);
return response()->json([
'success' => true,
'shipment_number' => $result['shipmentNumber'],
'label_path' => $result['label_path']
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'error' => $e->getMessage()
], 422);
}
}
}
```
## 8. Facade-Verwendung
```php
use DHL;
// Label erstellen
$result = DHL::createLabel($orderData);
// Tracking-Status abrufen
$status = DHL::getTrackingStatus('1234567890');
// Retourenlabel erstellen
$return = DHL::createReturn($returnData);
```
## Troubleshooting
### Composer-Probleme
```bash
# Cache leeren
composer dump-autoload
php artisan config:clear
php artisan cache:clear
# Paket neu installieren
composer remove acme/laravel-dhl
composer install
```
### Migration-Probleme
```bash
# Migrations-Status prüfen
php artisan migrate:status
# Rollback und neu migrieren
php artisan migrate:rollback --step=2
php artisan migrate
```
### Service-Provider nicht gefunden
```bash
# Auto-Discovery cache leeren
composer dump-autoload
php artisan package:discover
```

View file

@ -0,0 +1,195 @@
# DHL Versandmodul - Optimierter Programmierplan (MVP)
## Bewertung der Optimierungsvorschläge
### ✅ Akzeptierte Optimierungen:
1. **Vereinfachtes Datenbank-Schema**: Eine `dhl_shipments` Tabelle mit `type` Spalte für Outbound/Return
2. **Tracking vereinfacht**: Aktueller Status in Haupttabelle, separate Events-Tabelle für Phase 2
3. **Konsolidierte Service-Klasse**: Ein `DhlApiService` statt drei separate Services
4. **Advanced Features verschoben**: Excel Export, Webhooks, Notifications als Phase 2
### 🔄 Kleine Anpassungen am Vorschlag:
- Jobs bleiben getrennt (CreateShipment, Cancel, ReturnLabel) für bessere Queue-Verwaltung
- Tracking-Job für Scheduler-Integration beibehalten
- Admin-Navigation früher implementieren für bessere UX
---
# Finaler Optimierter Plan - DHL Modul (MVP)
## Phase 1: Fundament und Kernlogik (8 Schritte)
### 1. Setup & Dependencies ✅ AKTUALISIERT
- ~~Composer: `dhl-sdk-api-php/dhl-sdk-bcs` installieren~~ (veraltet, Probleme mit DHL-Login)
- **NEUER ANSATZ**: Eigenständiges Laravel-Paket `packages/acme-laravel-dhl`
- Direkte DHL API-Integration über HTTP-Client (Guzzle)
- Queue-System aktivieren (database driver)
### 2. Konfiguration
- `config/dhl.php` erstellen (API-Credentials, Absenderadressen)
- Environment-Variablen in `.env` definieren
### 3. Datenbank & Model ✅ ABGESCHLOSSEN
- Migration für `dhl_shipments` Tabelle: ✅
- order_id, dhl_shipment_no, type (outbound/return)
- related_shipment_id (für Retouren), weight_kg, label_path
- status, tracking_status, last_tracked_at
- api_response_data (JSON), created_at, updated_at
- `DhlShipment` Model mit vollständigen Relationships ✅
- `DhlTrackingEvent` Model für detaillierte Events ✅
### 4. Zentraler Service ✅ ABGESCHLOSSEN
- ~~`DhlApiService` als Wrapper um DHL SDK~~ (SDK-Ansatz verworfen)
- **NEUE STRUKTUR**:
- `DhlClient` - HTTP-Client für API-Kommunikation ✅
- `ShippingService` - Label-Erstellung und -Verwaltung ✅
- `TrackingService` - Status-Tracking und -Updates ✅
- `ReturnsService` - Retourenlabel-Management ✅
- Vollständige Error-Behandlung und Validation ✅
### 5. Queue Jobs
- `CreateShipmentJob` für asynchrone Label-Erstellung
- `CancelShipmentJob` für Stornierungen
- `CreateReturnLabelJob` für Retourenetiketten
### 6. Controller Foundation
- `DhlShipmentController` mit store/show/cancel Methoden
- Basis-Routen in `routes/web.php`
### 7. Error Handling & Logging ✅ ABGESCHLOSSEN
- Exception-Handling für DHL API-Fehler ✅
- HTTP-Status spezifische Exceptions
- Retry-Mechanismus (3x mit 300ms Delay)
- Timeout-Handling (30s)
- Aussagekräftige Fehlermeldungen ✅
- Validation aller Eingabedaten ✅
### 8. Basic Testing
- Unit-Tests für `DhlApiService`
- Feature-Tests für Controller
---
## Phase 2: Admin-Oberfläche (6 Schritte)
### 9. Navigation & Layout
- Admin-Menüpunkt "DHL Cockpit"
- Bootstrap 5 Layout für DHL-Views
### 10. Cockpit (Zentrale Übersicht)
- `index()` Methode im Controller
- `cockpit.blade.php` View mit DataTable
- Filter: Datum, Status, Sendungstyp
### 11. Einzelaktionen
- "Label herunterladen" Button
- "Sendung stornieren" Button mit Confirmation
### 12. Batch-Operationen
- Mehrere Sendungen auswählen
- Stapeldruck von Labels
- Massen-Stornierung
### 13. Integration in Bestellverwaltung
- DHL-Button in Bestelldetails
- Direkter Zugang zu Sendungsinformationen
### 14. Status-Anzeige & Feedback
- Alert-System für Erfolgsmeldungen/Fehler
- Real-time Status-Updates
---
## Phase 3: Tracking und Retouren (5 Schritte)
### 15. Tracking-System
- `UpdateTrackingStatusJob` für automatische Updates
- Artisan Command `dhl:update-tracking`
- Scheduler-Integration
### 16. Tracking-Anzeige
- Tracking-Status im Cockpit
- Tracking-Details in Sendungsansicht
- Timeline-View für Sendungsverlauf
### 17. Retouren-Management
- Button "Retourenlabel erstellen"
- Retouren-Workflow im Admin
- Verknüpfung Original-Sendung ↔ Retoure
### 18. Customer-Interface (Basic)
- Einfache öffentliche Tracking-Seite
- Tracking-Nummer-Eingabe
### 19. Notifications (Basic)
- E-Mail bei Sendungserstellung
- E-Mail bei wichtigen Status-Änderungen
---
## Phase 4: Finalisierung & Polish (4 Schritte)
### 20. Performance-Optimierung
- Eager Loading für Relationships
- Caching für häufige API-Abfragen
- Database-Indexe
### 21. Comprehensive Testing
- Integration-Tests für DHL SDK
- Browser-Tests für Admin-Interface
- Error-Scenario Tests
### 22. Documentation
- Benutzerhandbuch für Shop-Mitarbeiter
- Code-Dokumentation
- API-Dokumentation
### 23. Production-Ready Features
- Environment-spezifische Konfiguration
- Monitoring & Alerting
- Backup & Recovery-Konzept
---
## Optionale Erweiterungen (Phase 2 des Projekts)
### Advanced Features (später):
- Separate `dhl_tracking_events` Tabelle für detaillierte Historie
- Excel/CSV Export von Sendungsdaten
- Webhook-Integration für automatische Updates
- SMS-Notifications
- Erweiterte Reporting-Features
- Multi-Tenant Support
- API für externe Systeme
---
**Total: 23 konkrete Schritte** für ein vollständig funktionsfähiges DHL Versandmodul
Dieser Plan fokussiert sich auf die Kernfunktionalitäten und liefert schnell ein nutzbares Ergebnis.

120
dev/dhl-modul/README.md Normal file
View file

@ -0,0 +1,120 @@
# DHL Versandmodul - Entwicklungsdokumentation
## Projektübersicht
Laravel 11 DHL Versandmodul als eigenständiges Paket mit direkter DHL API-Integration.
## Architektur-Überarbeitungen ✅
**August 2025**: Wechsel von SDK-Ansatz zu eigenständigem Laravel-Paket
### Grund für Überarbeitung
- `christoph-schaeffer/dhl-business-shipping` SDK ist veraltet
- Probleme mit DHL-Login im alten SDK
- Bessere Kontrolle durch direkte API-Integration
### Neue Architektur
- **Eigenständiges Laravel-Paket**: `packages/acme-laravel-dhl`
- **Direkte DHL API**: HTTP-Client statt SDK-Wrapper
- **Vereinfachtes Schema**: Eine `dhl_package_shipments` Tabelle für Outbound/Returns
- **Moderne Laravel-Patterns**: Service Provider, Facades, Models
## Aktueller Entwicklungsstand ✅
### Phase 1: Paket-Grundlagen (ABGESCHLOSSEN)
- ✅ **Code Refactoring**: Services mit besserer Lesbarkeit
- ✅ **Datenbankschema**: Vereinfacht nach optimiertem Plan
- ✅ **Error Handling**: Umfassende Exception-Behandlung
- ✅ **Models**: DhlShipment + DhlTrackingEvent, inkl. Relationen
- ✅ **Services**: ShippingService, TrackingService, ReturnsService
- ✅ **HTTP-Client**: DhlClient mit Retry-Mechanismus
### Phase 2: Integration (ABGESCHLOSSEN)
- ✅ **Paket-Registration**: In Hauptprojekt eingebunden
- ✅ **API-Setup**: Credentials und Konfiguration
- ✅ **Admin-Controller**: Backend-Integration
- ✅ **Queue-Jobs**: Asynchrone Verarbeitung
### Phase 3: Admin-Interface (TEILWEISE ABGESCHLOSSEN)
- ✅ **DHL Cockpit**: Sendungsübersicht mit serverseitigem DataTables
- ✅ **Label-Management**: Download und Druck
- [ ] **Order-Integration**: DHL-Buttons in Bestellverwaltung
### Phase 4: Advanced Features (NÄCHSTE SCHRITTE)
- [ ] **Tracking-Automation**: Scheduler-Integration
- [ ] **Notifications**: E-Mail-Benachrichtigungen
- [ ] **Testing**: Unit/Feature Tests
- [ ] **Documentation**: Benutzerhandbuch
## Optimierungen und Änderungen (Letzte Updates)
- **Performance-Boost im Cockpit**: Die Sendungsübersicht wurde auf serverseitiges DataTables umgestellt, was die Ladezeiten bei großen Datenmengen drastisch reduziert.
- **Erweiterte Druckformate**: Das Modul unterstützt jetzt die Konfiguration von `print_format` und `retoure_print_format` via `.env` oder Admin-Einstellungen. Diese werden als Query-Parameter an die DHL API übergeben, um das physische Layout der Etiketten (z.B. A4, 910-300-700) zu steuern.
- **Speicherung des Routing-Codes**: Der `routing_code` (Leitcode) von DHL wird nun aus der API-Antwort extrahiert und in der Datenbank in der Spalte `dhl_package_shipments.routing_code` gespeichert.
- **API-Antwort-Parsing verbessert**: Die Logik wurde angepasst, um die Sendungsdaten korrekt aus dem `items`-Array der API-Antwort zu extrahieren.
- **Code optimiert**: Verbesserte Error-Handling, Logging in Services und Client.
- **Fehler behoben**: Namenskonflikte (Shipment -> DhlShipment), erweiterte Status-Mappings, robuste Validierungen.
- **Optionale Queues**: Konfiguriert via 'use_queue' in `config/dhl.php`. Synchron in Entwicklung, asynchron bei hoher Last.
## Dateistruktur
### Aktuelle Dokumentation
- `AKTUALISIERUNG-PAKET-ANSATZ.md` - Überarbeitungsdetails
- `PLAN-OPTIMIERT.md` - Ursprünglicher Plan (aktualisiert)
- `PAKET-INSTALLATION.md` - Setup-Anleitung
- `OPTIMIERUNGEN.md` - Details zu früheren Optimierungen
### Paket-Struktur
```
packages/acme-laravel-dhl/
├── config/dhl.php # Konfiguration
├── database/migrations/ # Datenbankstruktur
├── src/
│ ├── Services/ # Business Logic
│ ├── Models/ # Eloquent Models
│ ├── Support/ # HTTP Client
│ └── DhlServiceProvider.php # Laravel Integration
```
## Technische Highlights
### Moderne Laravel-Integration
- **PSR-4 Autoloading**: Sauberer Namespace
- **Service Provider**: Auto-Discovery Support
- **Facades**: `DHL::createLabel()` Syntax
- **Config Publishing**: `php artisan vendor:publish`
### Robuste API-Integration
- **Retry-Mechanismus**: 3 Versuche mit ansteigendem Delay
- **Timeout-Handling**: 30 Sekunden pro Request
- **HTTP-Status Mapping**: Spezifische Exceptions
- **Request/Response Logging**: Vollständige Nachverfolgung
- **Flexible API-Parameter**: Unterstützt Body-Payload und Query-Parameter bei POST-Requests.
### Optimiertes Datenbankschema
- **Eine Haupttabelle**: `dhl_package_shipments` für Outbound + Returns
- **Zusätzliche Felder**: `routing_code` für interne DHL-Logistik
- **Efficient Indexing**: Performance-optimiert
- **JSON-Felder**: Flexible API-Daten-Speicherung
- **Self-References**: Retour-Verknüpfungen
## Nächste Schritte
1. **Order-Integration** abschließen (Buttons in Bestelldetails).
2. **Tracking-Automatisierung** implementieren.
3. **Umfassende Tests** für die neuen Features schreiben.
---
**Status**: Admin-Cockpit implementiert und performant, Kernfunktionalität stabil.

View file

@ -0,0 +1,136 @@
# Schritt 3: Datenbank & Model - ABGESCHLOSSEN ✅
## Durchgeführte Arbeiten
### 3.1 Migration für dhl_shipments Tabelle erstellt ✅
- **File**: `database/migrations/2025_08_19_155158_create_dhl_shipments_table.php`
- **Status**: Erfolgreich migriert
- **Tabelle**: `dhl_shipments`
#### Tabellenstruktur:
```sql
- id (bigint unsigned, Primary Key)
- shopping_order_id (int unsigned, Foreign Key -> shopping_orders.id)
- related_shipment_id (bigint unsigned, Self-Reference für Retouren)
- shipment_number (varchar, DHL Sendungsnummer)
- tracking_number (varchar, DHL Tracking-Nummer)
- type (enum: 'outbound', 'return')
- weight, length, width, height (Paketdaten)
- product_code (varchar, DHL Produktcode)
- services (json, Zusätzliche Services)
- label_path, label_format, label_printed (Label-Management)
- status (enum: created, submitted, in_transit, delivered, returned, cancelled, failed)
- tracking_status, tracking_details, last_tracked_at (Tracking)
- recipient_* (vollständige Empfängeradresse)
- api_request_data, api_response_data, api_errors (API Debugging)
- shipping_cost, currency (Kostenabrechnung)
- notes, metadata (Zusätzliche Daten)
- shipped_at, delivered_at (Timestamps)
- created_at, updated_at (Standard Laravel Timestamps)
```
#### Indizes:
- shopping_order_id (Performance)
- shipment_number (DHL Suche)
- tracking_number (Tracking-Suche)
- [type, status] (Kombiniert für Filter)
- created_at (Chronologische Sortierung)
#### Foreign Key Constraints:
- shopping_order_id → shopping_orders.id (CASCADE DELETE)
- related_shipment_id → dhl_shipments.id (SET NULL)
### 3.2 DhlShipment Model erstellt ✅
- **File**: `app/Models/DhlShipment.php`
- **Status**: Vollständig implementiert
- **Namespace**: `App\Models\DhlShipment`
#### Model Features:
- ✅ **Vollständige Eloquent Konfiguration** (fillable, casts, table)
- ✅ **Konstanten für Status und Typen**
- ✅ **Relationships**: belongsTo ShoppingOrder, self-reference für Retouren
- ✅ **Scopes**: outbound(), returns(), active(), trackable()
- ✅ **Helper Methods**: isOutbound(), isReturn(), canBeCancelled(), hasTracking(), hasLabel()
- ✅ **Accessors**: getRecipientAddressAttribute(), getDimensionsAttribute(), getStatusLabelAttribute()
- ✅ **Boot Method**: Setzt Default-Werte aus Konfiguration
#### Konstanten:
```php
// Typen
const TYPE_OUTBOUND = 'outbound';
const TYPE_RETURN = 'return';
// Status
const STATUS_CREATED = 'created';
const STATUS_SUBMITTED = 'submitted';
const STATUS_IN_TRANSIT = 'in_transit';
const STATUS_DELIVERED = 'delivered';
const STATUS_RETURNED = 'returned';
const STATUS_CANCELLED = 'cancelled';
const STATUS_FAILED = 'failed';
```
### 3.3 ShoppingOrder Model erweitert ✅
- **File**: `app/Models/ShoppingOrder.php`
- **Erweitert um DHL Relationships**
#### Neue Methods:
```php
dhlShipments() // Alle DHL Sendungen
dhlOutboundShipments() // Nur Versand-Sendungen
dhlReturnShipments() // Nur Retour-Sendungen
hasDhlShipments(): bool // Prüft ob DHL Sendungen existieren
getLatestDhlShipment() // Neueste DHL Sendung
```
## Optimierungen implementiert
### MVP-Fokus erreicht ✅
- **Eine zentrale Tabelle** statt separater Tabellen für Retouren
- **Vollständige Empfängeradresse** in der Tabelle (kein Join nötig)
- **JSON-Felder** für flexible API-Daten und Services
- **Self-Reference** für Retouren über `related_shipment_id`
### Performance optimiert ✅
- **Strategische Indizes** für häufige Abfragen
- **Efficient Relationships** mit korrekten Foreign Key Typen
- **Scopes** für häufige Filter-Operationen
- **Casts** für automatische Datentyp-Konvertierung
### Developer Experience ✅
- **Ausführliche PHPDoc** für IDE-Support
- **Helper Methods** für Business Logic
- **Konstanten** statt Magic Strings
- **Accessors** für formatierte Ausgaben
## Bugfixes durchgeführt ✅
- **Foreign Key Type Mismatch** behoben: `unsignedInteger` statt `unsignedBigInteger` für shopping_order_id
- **Migration Konflikt** gelöst: Tabelle manuell entfernt und korrekt migriert
## Verifikation ✅
```bash
# Migration erfolgreich
php artisan migrate
# ✅ 2025_08_19_155158_create_dhl_shipments_table: 78.05ms DONE
# Model lädt korrekt
php artisan tinker --execute="use App\Models\DhlShipment; echo DhlShipment::class;"
# ✅ App\Models\DhlShipment
# Relationships funktionieren
php artisan tinker --execute="use App\Models\ShoppingOrder; ShoppingOrder::first()->dhlShipments()->count();"
# ✅ 0 (erwartungsgemäß, da noch keine DHL Shipments existieren)
```
## Nächste Schritte
- **Schritt 4**: Zentraler `DhlApiService` als Wrapper um das DHL SDK
- Integration der christoph-schaeffer/dhl-business-shipping Library
- Erste API-Verbindung und Test-Calls
## Files Created/Modified
- ✅ `database/migrations/2025_08_19_155158_create_dhl_shipments_table.php` (neu)
- ✅ `app/Models/DhlShipment.php` (neu)
- ✅ `app/Models/ShoppingOrder.php` (erweitert)
## Technical Debt: NONE ✨
Alle Optimierungen erfolgreich implementiert, keine bekannten Probleme.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,199 @@
# DHL Laravel Package
A comprehensive Laravel package for DHL shipping, tracking, and returns functionality with direct API integration.
## Features
- **Direct DHL API Integration**: No SDK dependencies, full control over API calls
- **German Address Parsing**: Automatic extraction of house numbers from German addresses
- **Queue Support**: Optional asynchronous processing for high-volume operations
- **Comprehensive Tracking**: Full tracking event history and status management
- **Return Label Generation**: Easy return shipment creation
- **Retry Mechanisms**: Built-in retry logic for API calls and file operations
- **Laravel Integration**: Native service providers, facades, and Eloquent models
## Installation
### 1. Add to composer.json
```json
{
"repositories": [
{
"type": "path",
"url": "./packages/acme-laravel-dhl"
}
],
"require": {
"acme/laravel-dhl": "*"
}
}
```
### 2. Install the package
```bash
composer update acme/laravel-dhl
```
### 3. Publish configuration and run migrations
```bash
php artisan vendor:publish --provider="Acme\Dhl\DhlServiceProvider" --tag="config"
php artisan migrate
```
## Configuration
Add the following environment variables to your `.env` file:
```env
# DHL API Configuration
DHL_BASE_URL=https://api-eu.dhl.com
DHL_API_KEY=your_api_key_here
DHL_USERNAME=your_username
DHL_PASSWORD=your_password
DHL_BILLING_NUMBER=your_billing_number
# DHL Default Settings
DHL_PRODUCT=V01PAK
DHL_LABEL_FORMAT=PDF
DHL_PROFILE=STANDARD_GRUPPENPROFIL
# Optional Queue Settings
DHL_USE_QUEUE=false
```
## Usage
### Creating a Shipment Label
```php
use DHL;
$orderData = [
'order_id' => 12345,
'weight_kg' => 2.5,
'shipper' => [
'name' => 'Your Company',
'street' => 'Musterstraße 123', // House number will be auto-parsed
'postalCode' => '12345',
'city' => 'Berlin',
'country' => 'DE'
],
'consignee' => [
'name' => 'Customer Name',
'street' => 'Kundenstraße 456', // House number will be auto-parsed
'postalCode' => '54321',
'city' => 'Munich',
'country' => 'DE'
]
];
$result = DHL::createLabel($orderData);
// Returns: ['shipmentNumber' => '123...', 'label_path' => 'dhl/labels/...']
```
### German Address Parsing
The package automatically handles German address formats by extracting house numbers from the street field:
```php
// Input: "Musterstraße 123a"
// Parsed to: street="Musterstraße", houseNumber="123a"
// Supported formats:
"Musterstraße 123" -> street: "Musterstraße", number: "123"
"Am Markt 7" -> street: "Am Markt", number: "7"
"Karl-Marx-Straße 156" -> street: "Karl-Marx-Straße", number: "156"
"Lindenstraße 1-3" -> street: "Lindenstraße", number: "1-3"
"Muster Str. 99" -> street: "Muster Str.", number: "99"
```
### Tracking Shipments
```php
use DHL;
// Get current tracking status
$status = DHL::track('1234567890123');
// Returns detailed tracking information including events
```
### Creating Return Labels
```php
use DHL;
$returnData = [
'original_shipment_id' => 1,
'return_reason' => 'Customer return',
// ... address data
];
$return = DHL::createReturn($returnData);
```
### Using Services Directly
```php
use Acme\Dhl\Services\ShippingService;
use Acme\Dhl\Services\TrackingService;
// Dependency injection in controllers
public function createLabel(ShippingService $shipping, Request $request)
{
$result = $shipping->createLabel($request->validated());
return response()->json($result);
}
```
## Database Schema
The package creates the following tables:
- `dhl_shipments` - Main shipment records (outbound and returns)
- `dhl_tracking_events` - Tracking event history
## Queue Configuration
For high-volume applications, enable queue processing:
```env
DHL_USE_QUEUE=true
```
When enabled, label creation and tracking updates will be processed asynchronously.
## Error Handling
The package includes comprehensive error handling:
- `DhlApiException` - General API errors
- `DhlAuthenticationException` - Authentication failures
- `DhlValidationException` - Data validation errors
## Testing
Run the included tests:
```bash
vendor/bin/phpunit packages/acme-laravel-dhl/tests/
```
## Address Parsing Tests
The package includes comprehensive tests for German address parsing:
```bash
vendor/bin/phpunit packages/acme-laravel-dhl/tests/Unit/AddressParserTest.php
```
## Logging
All API calls, address parsing, and errors are logged using Laravel's logging system. Check your logs for debugging information.
## License
MIT License

View file

@ -0,0 +1,30 @@
{
"name": "acme/laravel-dhl",
"description": "DHL (Post & Paket DE) Shipping, Returns & Tracking for Laravel.",
"type": "library",
"license": "MIT",
"require": {
"php": "^8.1",
"illuminate/support": "^10.0|^11.0",
"illuminate/http": "^10.0|^11.0",
"illuminate/database": "^10.0|^11.0",
"illuminate/queue": "^10.0|^11.0",
"illuminate/events": "^10.0|^11.0",
"guzzlehttp/guzzle": "^7.0"
},
"autoload": {
"psr-4": {
"Acme\\Dhl\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Acme\\Dhl\\DhlServiceProvider"
],
"aliases": {
"DHL": "Acme\\Dhl\\Facades\\DHL"
}
}
}
}

View file

@ -0,0 +1,13 @@
<?php
return [
'base_url' => env('DHL_BASE_URL', 'https://api-eu.dhl.com'),
'api_key' => env('DHL_API_KEY'),
'username' => env('DHL_USERNAME'),
'password' => env('DHL_PASSWORD'),
'billing_number' => env('DHL_BILLING_NUMBER'),
'default_product' => env('DHL_PRODUCT', 'V01PAK'),
'label_format' => env('DHL_LABEL_FORMAT', 'PDF'),
'profile' => env('DHL_PROFILE', 'STANDARD_GRUPPENPROFIL'),
'webhook' => ['enabled' => env('DHL_WEBHOOK_ENABLED', false), 'secret' => env('DHL_WEBHOOK_SECRET'), 'route' => env('DHL_WEBHOOK_ROUTE', 'dhl/webhooks/tracking')],
'use_queue' => env('DHL_USE_QUEUE', false), // Set to true to enable queue jobs for async processing
];

View file

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
// Anonymous class for migration (Laravel standard since 8.x)
return new class extends Migration
{
public function up(): void
{
Schema::create('dhl_package_shipments', function (Blueprint $table) {
$table->id();
// Basic shipment info
$table->unsignedBigInteger('order_id')->nullable()->index();
$table->string('dhl_shipment_no')->nullable()->index();
$table->enum('type', ['outbound', 'return'])->default('outbound');
$table->unsignedBigInteger('related_shipment_id')->nullable()->index(); // For returns
// Product and billing
$table->string('product_code')->default('V01PAK');
$table->string('billing_number')->nullable();
$table->decimal('weight_kg', 8, 3)->nullable();
// Label information
$table->string('label_format')->default('PDF');
$table->string('label_path')->nullable();
// Status tracking (simplified)
$table->string('status')->default('created'); // created, in_transit, delivered, canceled, etc.
$table->string('tracking_status')->nullable(); // Last DHL status text
$table->timestamp('last_tracked_at')->nullable();
// API response data
$table->json('api_response_data')->nullable();
$table->timestamps();
// Foreign key constraints
$table->foreign('related_shipment_id')->references('id')->on('dhl_package_shipments')->onDelete('set null');
// Indexes for performance
$table->index(['order_id', 'type']);
$table->index(['status', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('dhl_package_shipments');
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('shipments', function (Blueprint $t) {
$t->id();
$t->unsignedBigInteger('order_id')->nullable();
$t->string('carrier')->default('dhl');
$t->string('dhl_shipment_no')->nullable()->index();
$t->string('product_code')->nullable();
$t->string('billing_number')->nullable();
$t->decimal('weight_kg', 8, 3)->nullable();
$t->string('status')->default('created');
$t->string('label_format')->nullable();
$t->string('label_path')->nullable();
$t->json('meta')->nullable();
$t->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('shipments');
}
};

View file

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('shipment_labels', function (Blueprint $t) {
$t->id();
$t->foreignId('shipment_id')->constrained('shipments')->cascadeOnDelete();
$t->string('format')->default('PDF');
$t->string('path');
$t->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('shipment_labels');
}
};

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('dhl_package_tracking_events', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('shipment_id')->index();
$table->string('status_code')->nullable();
$table->string('status_text')->nullable();
$table->string('location')->nullable();
$table->timestamp('event_time')->nullable();
$table->json('raw')->nullable(); // Full event data from API
$table->timestamps();
$table->foreign('shipment_id')->references('id')->on('dhl_package_shipments')->onDelete('cascade');
$table->index(['shipment_id', 'event_time']);
});
}
public function down(): void
{
Schema::dropIfExists('dhl_package_tracking_events');
}
};

View file

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('tracking_events', function (Blueprint $t) {
$t->id();
$t->foreignId('shipment_id')->constrained('shipments')->cascadeOnDelete();
$t->string('status_code')->nullable();
$t->string('status_text')->nullable();
$t->string('location')->nullable();
$t->timestamp('event_time')->nullable();
$t->json('raw')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('tracking_events');
}
};

View file

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('return_labels', function (Blueprint $t) {
$t->id();
$t->unsignedBigInteger('order_id')->nullable()->index();
$t->string('dhl_return_no')->nullable()->index();
$t->string('label_path')->nullable();
$t->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('return_labels');
}
};

View file

@ -0,0 +1,6 @@
<?php
use Illuminate\Support\Facades\Route;
use Acme\Dhl\Http\Controllers\TrackingWebhookController;
Route::post(config('dhl.webhook.route', 'dhl/webhooks/tracking'), [TrackingWebhookController::class, 'handle'])->name('dhl.webhook.tracking');

View file

@ -0,0 +1,40 @@
<?php
namespace Acme\Dhl;
class DhlManager
{
public function __construct(protected Support\DhlClient $client, protected Services\ShippingService $shipping, protected Services\TrackingService $tracking, protected Services\ReturnsService $returns) {}
/**
* @param array $orderData
* @return array
*/
public function createLabel(array $orderData): array
{
return $this->shipping->createLabel($orderData);
}
/**
* @param string $shipmentNumber
* @return bool
*/
public function cancelLabel(string $shipmentNumber): bool
{
return $this->shipping->cancelLabel($shipmentNumber);
}
/**
* @param string $trackingNumber
* @return array
*/
public function track(string $trackingNumber): array
{
return $this->tracking->fetchStatus($trackingNumber);
}
/**
* @param array $returnData
* @return array
*/
public function createReturn(array $returnData): array
{
return $this->returns->createReturn($returnData);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Acme\Dhl;
use Illuminate\Support\ServiceProvider;
class DhlServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../config/dhl.php', 'dhl');
// Bind DhlClient as a singleton
$this->app->singleton(Support\DhlClient::class, function ($app) {
return new Support\DhlClient(
config('dhl.base_url'),
config('dhl.api_key'),
config('dhl.username'),
config('dhl.password')
);
});
// Bind services as singletons
$this->app->singleton(Services\ShippingService::class, function ($app) {
return new Services\ShippingService($app->make(Support\DhlClient::class));
});
$this->app->singleton(Services\TrackingService::class, function ($app) {
return new Services\TrackingService($app->make(Support\DhlClient::class));
});
$this->app->singleton(Services\ReturnsService::class, function ($app) {
return new Services\ReturnsService($app->make(Support\DhlClient::class));
});
// Bind the main manager class
$this->app->singleton(DhlManager::class, function ($app) {
return new DhlManager(
$app->make(Support\DhlClient::class),
$app->make(Services\ShippingService::class),
$app->make(Services\TrackingService::class),
$app->make(Services\ReturnsService::class)
);
});
}
public function boot(): void
{
$this->publishes([__DIR__ . '/../config/dhl.php' => config_path('dhl.php')], 'dhl-config');
$this->publishes([__DIR__ . '/../database/migrations/' => database_path('migrations')], 'dhl-migrations');
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
if (config('dhl.webhook.enabled')) {
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
}
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Acme\Dhl\Exceptions;
class DhlApiException extends \Exception
{
//
}

View file

@ -0,0 +1,8 @@
<?php
namespace Acme\Dhl\Exceptions;
class DhlAuthenticationException extends DhlApiException
{
//
}

View file

@ -0,0 +1,8 @@
<?php
namespace Acme\Dhl\Exceptions;
class DhlValidationException extends DhlApiException
{
//
}

View file

@ -0,0 +1,13 @@
<?php
namespace Acme\Dhl\Facades;
use Illuminate\Support\Facades\Facade;
class DHL extends Facade
{
protected static function getFacadeAccessor()
{
return \Acme\Dhl\DhlManager::class;
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Acme\Dhl\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Log;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Models\DhlTrackingEvent;
class TrackingWebhookController extends Controller
{
public function handle(Request $r)
{
$secret = config('dhl.webhook.secret');
if ($secret && $r->header('X-DHL-Secret') !== $secret) {
abort(401, 'Invalid webhook secret');
}
$payload = $r->all();
Log::info('DHL webhook received', ['payload' => $payload]);
$events = data_get($payload, 'events', []);
foreach ($events as $e) {
$tracking = $e['trackingNumber'] ?? null;
if (!$tracking) continue;
$s = DhlShipment::firstOrCreate(['dhl_shipment_no' => $tracking], ['carrier' => 'dhl', 'status' => 'unknown',]);
DhlTrackingEvent::create(['shipment_id' => $s->id, 'status_code' => $e['statusCode'] ?? null, 'status_text' => $e['status'] ?? null, 'location' => data_get($e, 'location'), 'event_time' => $e['timestamp'] ?? now(), 'raw' => $e,]);
$statusCode = $e['statusCode'] ?? '';
$newStatus = match (strtolower($statusCode)) {
'delivered' => 'delivered',
'returned' => 'returned',
'exception', 'failed' => 'failed',
'in_transit', 'transit' => 'in_transit',
default => $s->status
};
if ($newStatus !== $s->status) {
$s->status = $newStatus;
$s->save();
Log::info('Webhook updated shipment status', ['tracking' => $tracking, 'newStatus' => $newStatus]);
}
}
return response()->json(['ok' => true]);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Acme\Dhl\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Acme\Dhl\Services\ReturnsService;
use Illuminate\Support\Facades\Log;
class CreateReturnLabelJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public array $returnData) {}
public function handle(ReturnsService $service): void
{
try {
$service->createReturn($this->returnData);
Log::info('Queued return label creation successful');
} catch (\Exception $e) {
Log::error('Queued return label creation failed', ['error' => $e->getMessage()]);
$this->fail($e);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Acme\Dhl\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Acme\Dhl\Services\ShippingService;
use Illuminate\Support\Facades\Log;
class CreateShipmentJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public array $orderData) {}
public function handle(ShippingService $service): void
{
try {
$service->createLabel($this->orderData);
Log::info('Queued shipment creation successful');
} catch (\Exception $e) {
Log::error('Queued shipment creation failed', ['error' => $e->getMessage()]);
$this->fail($e);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Acme\Dhl\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Acme\Dhl\Services\TrackingService;
use Illuminate\Support\Facades\Log;
class SyncTrackingJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public array $trackingNumbers) {}
public function handle(TrackingService $t): void
{
foreach ($this->trackingNumbers as $trackingNumber) {
try {
$t->fetchStatus($trackingNumber);
Log::info('Synced tracking', ['trackingNumber' => $trackingNumber]);
} catch (\Exception $e) {
Log::error('Tracking sync failed', ['trackingNumber' => $trackingNumber, 'error' => $e->getMessage()]);
$this->fail($e);
}
}
}
}

View file

@ -0,0 +1,149 @@
<?php
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\ShoppingOrder;
/**
* DHL Shipment Model for both outbound shipments and returns
*/
class DhlShipment extends Model
{
protected $table = 'dhl_package_shipments';
protected $fillable = [
'order_id',
'dhl_shipment_no',
'routing_code',
'type',
'related_shipment_id',
'product_code',
'billing_number',
'weight_kg',
'label_format',
'label_path',
'status',
'tracking_status',
'last_tracked_at',
'api_response_data'
];
protected $casts = [
'api_response_data' => 'array',
'last_tracked_at' => 'datetime',
'weight_kg' => 'decimal:3'
];
public const STATUS_MAP = [
'pre-transit' => 'created',
'transit' => 'in_transit',
'out_for_delivery' => 'out_for_delivery',
'delivered' => 'delivered',
'exception' => 'exception',
'returned' => 'returned',
'failed' => 'failed',
'unknown' => 'unknown'
];
/**
* Get the tracking events for this shipment
*/
public function trackingEvents(): HasMany
{
return $this->hasMany(DhlTrackingEvent::class, 'shipment_id');
}
/**
* Get the related shopping order for this shipment
*/
public function shoppingOrder(): BelongsTo
{
return $this->belongsTo(ShoppingOrder::class, 'order_id');
}
/**
* Get the related shipment (for returns)
*/
public function relatedShipment(): BelongsTo
{
return $this->belongsTo(self::class, 'related_shipment_id');
}
/**
* Get returns for this shipment (if this is an outbound shipment)
*/
public function returns(): HasMany
{
return $this->hasMany(self::class, 'related_shipment_id');
}
/**
* Scope for outbound shipments
*/
public function scopeOutbound($query)
{
return $query->where('type', 'outbound');
}
/**
* Scope for return shipments
*/
public function scopeReturns($query)
{
return $query->where('type', 'return');
}
/**
* Scope for shipments by order
*/
public function scopeForOrder($query, int $orderId)
{
return $query->where('order_id', $orderId);
}
/**
* Check if this is a return shipment
*/
public function isReturn(): bool
{
return $this->type === 'return';
}
/**
* Check if this is an outbound shipment
*/
public function isOutbound(): bool
{
return $this->type === 'outbound';
}
/**
* Get the latest tracking status
*/
public function getLatestStatus(): ?string
{
return $this->trackingEvents()
->orderBy('event_time', 'desc')
->first()
?->status_text;
}
/**
* Check if shipment can be canceled
*/
public function canCancel(): bool
{
return in_array($this->status, ['created', 'pending']);
}
/**
* Check if shipment is delivered
*/
public function isDelivered(): bool
{
return $this->status === 'delivered';
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* DHL Tracking Event Model for detailed tracking history
*/
class DhlTrackingEvent extends Model
{
protected $table = 'dhl_package_tracking_events';
protected $fillable = [
'shipment_id',
'status_code',
'status_text',
'location',
'event_time',
'raw'
];
protected $casts = [
'raw' => 'array',
'event_time' => 'datetime'
];
/**
* Get the shipment this event belongs to
*/
public function shipment(): BelongsTo
{
return $this->belongsTo(DhlShipment::class, 'shipment_id');
}
/**
* Scope for events ordered by time
*/
public function scopeOrderedByTime($query, string $direction = 'desc')
{
return $query->orderBy('event_time', $direction);
}
/**
* Scope for recent events
*/
public function scopeRecent($query, int $days = 30)
{
return $query->where('event_time', '>=', now()->subDays($days));
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
class ReturnLabel extends Model
{
protected $fillable = ['order_id', 'dhl_return_no', 'label_path'];
}

View file

@ -0,0 +1,19 @@
<?php
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
class Shipment extends Model
{
protected $fillable = ['order_id', 'carrier', 'dhl_shipment_no', 'product_code', 'billing_number', 'weight_kg', 'status', 'label_format', 'label_path', 'meta'];
protected $casts = ['meta' => 'array',];
public function labels()
{
return $this->hasMany(ShipmentLabel::class);
}
public function events()
{
return $this->hasMany(TrackingEvent::class);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
class ShipmentLabel extends Model
{
protected $fillable = ['shipment_id', 'format', 'path'];
public function shipment()
{
return $this->belongsTo(Shipment::class);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
class TrackingEvent extends Model
{
public $timestamps = false;
protected $fillable = ['shipment_id', 'status_code', 'status_text', 'location', 'event_time', 'raw'];
protected $casts = ['raw' => 'array', 'event_time' => 'datetime',];
public function shipment()
{
return $this->belongsTo(Shipment::class);
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace Acme\Dhl\Services;
use Acme\Dhl\Support\DhlClient;
use Acme\Dhl\Models\DhlShipment;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Exception;
use Acme\Dhl\Jobs\CreateReturnLabelJob;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
/**
* DHL Returns Service for creating and managing return labels
*/
class ReturnsService
{
public function __construct(protected DhlClient $client) {}
/**
* Create a return label for a shipment
*
* @param array $returnData Return shipment data
* @return array Return label details including number and path
* @throws InvalidArgumentException When required data is missing
* @throws Exception When API request fails
*/
public function createReturn(array $returnData): array
{
$validatedData = $this->validateReturnData($returnData);
if (config('dhl.use_queue')) {
CreateReturnLabelJob::dispatch($validatedData);
return ['queued' => true];
}
$payload = $this->buildReturnPayload($returnData);
$response = $this->client->request('post', '/parcel/de/returns/v1/labels', $payload);
$returnNumber = $this->extractReturnNumber($response);
$labelBase64 = $this->extractLabelData($response);
$labelPath = $this->saveLabelFile($returnNumber, $labelBase64, $payload['labelFormat']);
$returnShipment = $this->createReturnRecord($returnData, $returnNumber, $labelPath, $response);
Log::info('Created return label', ['returnNumber' => $returnNumber]);
return [
'returnNumber' => $returnNumber,
'label_path' => $labelPath,
'returnShipment' => $returnShipment,
'raw' => $response
];
}
/**
* Get return shipment by return number
*
* @param string $returnNumber DHL return number
* @return DhlShipment|null Return shipment model or null if not found
*/
public function getReturnShipment(string $returnNumber): ?DhlShipment
{
return DhlShipment::where('dhl_shipment_no', $returnNumber)
->where('type', 'return')
->first();
}
/**
* Get all return shipments for an order
*
* @param int $orderId Order ID
* @return \Illuminate\Database\Eloquent\Collection Return shipments collection
*/
public function getOrderReturns(int $orderId): \Illuminate\Database\Eloquent\Collection
{
return DhlShipment::where('order_id', $orderId)
->where('type', 'return')
->get();
}
/**
* Get returns for a specific outbound shipment
*
* @param int $shipmentId Original outbound shipment ID
* @return \Illuminate\Database\Eloquent\Collection Related return shipments
*/
public function getShipmentReturns(int $shipmentId): \Illuminate\Database\Eloquent\Collection
{
return DhlShipment::where('related_shipment_id', $shipmentId)
->where('type', 'return')
->get();
}
/**
* Validate required return data
*/
private function validateReturnData(array $data): array
{
$validator = Validator::make($data, [
'original_shipment_id' => 'nullable|integer|exists:dhl_shipments,id',
'weight_kg' => 'nullable|numeric|min:0.1',
'shipper' => 'required|array',
'shipper.name' => 'required|string|max:50',
// Add similar rules as in ShippingService
'consignee' => 'required|array',
'consignee.name' => 'required|string|max:50',
// ... more fields
]);
if ($validator->fails()) {
throw new InvalidArgumentException($validator->errors()->first());
}
if (empty(config('dhl.billing_number'))) {
throw new InvalidArgumentException('DHL billing number must be configured');
}
return $validator->validated();
}
/**
* Build DHL return API payload
*/
private function buildReturnPayload(array $returnData): array
{
return [
'labelFormat' => $returnData['label_format'] ?? 'PDF',
'shipper' => $returnData['shipper'],
'consignee' => $returnData['consignee'],
'billingNumber' => config('dhl.billing_number')
];
}
/**
* Extract return number from API response
*/
private function extractReturnNumber(array $response): ?string
{
return data_get($response, 'returnShipmentNo')
?? data_get($response, 'shipments.0.returnShipmentNo');
}
/**
* Extract base64 label data from API response
*/
private function extractLabelData(array $response): ?string
{
return data_get($response, 'label');
}
/**
* Save return label file to storage
*/
private function saveLabelFile(?string $returnNumber, ?string $labelBase64, string $format): ?string
{
if (!$labelBase64 || !$returnNumber) {
return null;
}
$path = 'dhl/returns/' . $returnNumber . '.' . strtolower($format);
Storage::disk('local')->put($path, base64_decode($labelBase64));
return $path;
}
/**
* Create return shipment database record
*/
private function createReturnRecord(array $returnData, ?string $returnNumber, ?string $labelPath, array $response): DhlShipment
{
return DhlShipment::create([
'order_id' => $returnData['order_id'] ?? null,
'dhl_shipment_no' => $returnNumber,
'type' => 'return',
'related_shipment_id' => $returnData['original_shipment_id'] ?? null,
'label_format' => $returnData['label_format'] ?? 'PDF',
'label_path' => $labelPath,
'status' => 'created',
'api_response_data' => $response
]);
}
}

View file

@ -0,0 +1,480 @@
<?php
namespace Acme\Dhl\Services;
use Acme\Dhl\Support\DhlClient;
use Acme\Dhl\Models\DhlShipment;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use InvalidArgumentException;
use Exception;
use Acme\Dhl\Jobs\CreateShipmentJob;
use Illuminate\Support\Facades\Log;
/**
* DHL Shipping Service for creating and managing shipment labels
*/
class ShippingService
{
public function __construct(protected DhlClient $client) {}
/**
* Create a new DHL shipment label
*
* @param array $orderData Order and shipping data
* @return array Shipment details including number and label path
* @throws InvalidArgumentException When required data is missing
* @throws Exception When API request fails
*/
public function createLabel(array $orderData): array
{
Log::info('createLabel', $orderData);
$validatedData = $this->validateOrderData($orderData);
if (config('dhl.use_queue')) {
CreateShipmentJob::dispatch($validatedData);
return ['queued' => true];
}
return DB::transaction(function () use ($validatedData) {
$payload = $this->buildShipmentPayload($validatedData);
// Debug logging: Log the exact payload being sent to DHL API
Log::info('[DHL API] Sending payload to DHL', [
'endpoint' => '/parcel/de/shipping/v2/orders',
'payload' => $payload,
'payload_json' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
]);
try {
// Build query parameters for print format
$query = array_filter([
'printFormat' => $validatedData['print_format'] ?? null,
'retourePrintFormat' => $validatedData['retoure_print_format'] ?? null,
]);
$response = $this->client->request('post', '/parcel/de/shipping/v2/orders', $payload, $query);
Log::info('[DHL API] Response received', [
'response' => $response
]);
} catch (Exception $e) {
Log::error('[DHL API] Request failed', [
'error' => $e->getMessage(),
'payload' => $payload
]);
throw $e;
}
$shipmentNumber = $this->extractShipmentNumber($response);
$labelBase64 = $this->extractLabelData($response);
$shipment = $this->createShipmentRecord($validatedData, $payload, $response, $shipmentNumber);
$labelPath = $this->saveLabelFile($shipment, $labelBase64, $payload['shipments'][0]['print']['format']);
Log::info('Created shipment label', ['shipmentNumber' => $shipmentNumber]);
return [
'shipmentNumber' => $shipmentNumber,
'label_path' => $labelPath,
'shipment' => $shipment,
'raw' => $response
];
});
}
/**
* Cancel an existing DHL shipment
*
* @param string $shipmentNumber DHL shipment number
* @return bool Success status
* @throws Exception When cancellation fails
*/
public function cancelLabel(string $shipmentNumber): bool
{
if (empty($shipmentNumber)) {
throw new InvalidArgumentException('Shipment number is required');
}
$shipment = DhlShipment::where('dhl_shipment_no', $shipmentNumber)->first();
if (!$shipment || !$shipment->canCancel()) {
throw new InvalidArgumentException('Shipment cannot be canceled');
}
$this->client->request('delete', "/parcel/de/shipping/v2/orders/{$shipmentNumber}");
$shipment->update(['status' => 'canceled']);
Log::info('Canceled shipment', ['shipmentNumber' => $shipmentNumber]);
return true;
}
/**
* Validate required order data according to DHL API v2 specification
*/
private function validateOrderData(array $data): array
{
// Pre-process German addresses before validation
$data = $this->preprocessAddresses($data);
$validator = Validator::make($data, [
'order_id' => 'nullable|integer',
'weight_kg' => 'required|numeric|min:0.1|max:31.5', // DHL weight limit
'product_code' => 'nullable|string|in:V01PAK,V53PAK,V62WP,V07PAK',
'label_format' => 'nullable|string|in:PDF,ZPL',
'print_format' => 'nullable|string', // Values like A4, 910-300-700 etc.
'retoure_print_format' => 'nullable|string',
// Shipper validation (sender)
'shipper' => 'required|array',
'shipper.name' => 'required|string|max:50',
'shipper.name2' => 'nullable|string|max:50',
'shipper.street' => 'required|string|max:50',
'shipper.houseNumber' => 'required|string|max:10',
'shipper.postalCode' => 'required|string|max:10',
'shipper.city' => 'required|string|max:50',
'shipper.country' => 'required|string|size:2', // ISO 3166-1 alpha-2
'shipper.email' => 'nullable|email|max:100',
'shipper.phone' => 'nullable|string|max:20',
// Consignee validation (recipient)
'consignee' => 'required|array',
'consignee.name' => 'required|string|max:50',
'consignee.name2' => 'nullable|string|max:50',
'consignee.street' => 'required|string|max:50',
'consignee.houseNumber' => 'required|string|max:10',
'consignee.postalCode' => 'required|string|max:10',
'consignee.city' => 'required|string|max:50',
'consignee.country' => 'required|string|size:2',
'consignee.email' => 'nullable|email|max:100',
'consignee.phone' => 'nullable|string|max:20',
// Optional dimensions
'dimensions' => 'nullable|array',
'dimensions.length' => 'nullable|numeric|min:1|max:120',
'dimensions.width' => 'nullable|numeric|min:1|max:60',
'dimensions.height' => 'nullable|numeric|min:1|max:60',
// Optional services and reference
'services' => 'nullable|array',
'reference' => 'nullable|string|max:35', // DHL reference field limit
]);
if ($validator->fails()) {
throw new InvalidArgumentException($validator->errors()->first());
}
return $validator->validated();
}
/**
* Preprocess addresses to extract house numbers from street field
*/
private function preprocessAddresses(array $data): array
{
// Process shipper address
if (isset($data['shipper'])) {
$data['shipper'] = $this->parseAddressFields($data['shipper']);
}
// Process consignee address
if (isset($data['consignee'])) {
$data['consignee'] = $this->parseAddressFields($data['consignee']);
}
return $data;
}
/**
* Parse German address to extract street and house number
*/
private function parseAddressFields(array $addressData): array
{
// If houseNumber is already provided, use it
if (!empty($addressData['houseNumber'])) {
return $addressData;
}
// If no houseNumber provided, try to parse from street
if (empty($addressData['street'])) {
return $addressData;
}
$street = trim($addressData['street']);
$parsed = $this->parseGermanAddress($street);
// Only update if we successfully parsed both parts
if ($parsed['street'] && $parsed['houseNumber']) {
$addressData['street'] = $parsed['street'];
$addressData['houseNumber'] = $parsed['houseNumber'];
Log::info('Parsed German address', [
'original' => $street,
'parsed_street' => $parsed['street'],
'parsed_houseNumber' => $parsed['houseNumber']
]);
} elseif (!$parsed['houseNumber']) {
// If we can't parse house number, use a default
$addressData['houseNumber'] = '1';
Log::warning('Could not parse house number from address, using default', [
'street' => $street
]);
}
return $addressData;
}
/**
* Parse German address string to extract street name and house number
* Handles formats like: "Musterstraße 123", "Muster Str. 123a", "Am Markt 1-3"
*/
private function parseGermanAddress(string $address): array
{
$address = trim($address);
// Pattern to match German addresses
// Captures everything before the last word that contains numbers
$patterns = [
// "Musterstraße 123a" -> street: "Musterstraße", number: "123a"
'/^(.+?)\s+([0-9]+[a-zA-Z]?)\s*$/',
// "Am Markt 1-3" -> street: "Am Markt", number: "1-3"
'/^(.+?)\s+([0-9]+[-\/][0-9]+[a-zA-Z]?)\s*$/',
// "Muster Str. 123" -> street: "Muster Str.", number: "123"
'/^(.+?)\s+([0-9]+)\s*$/',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $address, $matches)) {
return [
'street' => trim($matches[1]),
'houseNumber' => trim($matches[2])
];
}
}
// If no pattern matches, return original street with empty house number
return [
'street' => $address,
'houseNumber' => null
];
}
/**
* Build DHL API v2 payload from order data
*
* Structure follows official DHL API v2 createOrders specification:
* https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2
*/
private function buildShipmentPayload(array $orderData): array
{
$productCode = $orderData['product_code'] ?? config('dhl.default_product', 'V01PAK');
$billingNumber = $this->getBillingNumberForProduct($productCode);
$payload = [
'profile' => config('dhl.profile', 'STANDARD_GRUPPENPROFIL'),
'shipments' => [[
'product' => $productCode,
'billingNumber' => $billingNumber,
// Shipper information (sender) - separate street and house number as per official spec
'shipper' => array_filter([
'name1' => $orderData['shipper']['name'] ?? '',
'name2' => !empty($orderData['shipper']['name2']) ? $orderData['shipper']['name2'] : null,
'addressStreet' => $orderData['shipper']['street'] ?? '',
'addressHouse' => $orderData['shipper']['houseNumber'] ?? null,
'postalCode' => $orderData['shipper']['postalCode'] ?? '',
'city' => $orderData['shipper']['city'] ?? '',
'country' => $this->convertCountryCode($orderData['shipper']['country'] ?? 'DE'),
'email' => !empty($orderData['shipper']['email']) ? $orderData['shipper']['email'] : null,
'phone' => !empty($orderData['shipper']['phone']) ? $orderData['shipper']['phone'] : null,
], function ($value) {
return $value !== null;
}),
// Consignee information (recipient) - separate street and house number as per official spec
'consignee' => array_filter([
'name1' => $orderData['consignee']['name'] ?? '',
'name2' => !empty($orderData['consignee']['name2']) ? $orderData['consignee']['name2'] : null,
'addressStreet' => $orderData['consignee']['street'] ?? '',
'addressHouse' => $orderData['consignee']['houseNumber'] ?? null,
'postalCode' => $orderData['consignee']['postalCode'] ?? '',
'city' => $orderData['consignee']['city'] ?? '',
'country' => $this->convertCountryCode($orderData['consignee']['country'] ?? 'DE'),
'email' => !empty($orderData['consignee']['email']) ? $orderData['consignee']['email'] : null,
'phone' => !empty($orderData['consignee']['phone']) ? $orderData['consignee']['phone'] : null,
], function ($value) {
return $value !== null;
}),
'details' => [
'weight' => [
'value' => ($orderData['weight_kg'] ?? 1.0) * 1000, // Convert kg to grams
'uom' => 'g'
]
],
'print' => [
'format' => $orderData['label_format'] ?? config('dhl.label_format', 'PDF')
]
]]
];
// Add dimensions if provided (convert cm to mm)
if (!empty($orderData['dimensions'])) {
$payload['shipments'][0]['details']['dim'] = [
'uom' => 'mm',
'length' => ($orderData['dimensions']['length'] ?? 30) * 10, // cm to mm
'width' => ($orderData['dimensions']['width'] ?? 25) * 10, // cm to mm
'height' => ($orderData['dimensions']['height'] ?? 10) * 10, // cm to mm
];
}
// Add custom reference if provided
if (!empty($orderData['reference'])) {
$payload['shipments'][0]['refNo'] = $orderData['reference'];
}
return $payload;
}
/**
* Convert 2-letter country code to 3-letter country code for DHL API
*/
private function convertCountryCode(string $countryCode): string
{
$countryMap = [
'DE' => 'DEU',
'AT' => 'AUT',
'CH' => 'CHE',
'US' => 'USA',
'GB' => 'GBR',
'FR' => 'FRA',
'IT' => 'ITA',
'ES' => 'ESP',
'NL' => 'NLD',
'BE' => 'BEL',
'PL' => 'POL',
'CZ' => 'CZE',
'DK' => 'DNK',
'SE' => 'SWE',
'NO' => 'NOR',
];
return $countryMap[strtoupper($countryCode)] ?? 'DEU';
}
/**
* Get the correct billing number for the given product code
*/
private function getBillingNumberForProduct(string $productCode): string
{
// Try to get account number from config by product code
$accountNumber = config("dhl.account_numbers.{$productCode}");
if ($accountNumber) {
return $accountNumber;
}
// Try to get from admin settings via Setting model
try {
$settingKey = 'dhl_account_' . strtolower($productCode);
$accountNumber = \App\Models\Setting::getContentBySlug($settingKey);
if ($accountNumber) {
return $accountNumber;
}
} catch (\Exception $e) {
Log::warning('Could not load DHL account number from settings', [
'product_code' => $productCode,
'setting_key' => $settingKey,
'error' => $e->getMessage()
]);
}
// Fallback to default billing number
$defaultBillingNumber = config('dhl.billing_number') ?: config('dhl.account_numbers.default');
Log::warning('Using default billing number for product code', [
'product_code' => $productCode,
'billing_number' => $defaultBillingNumber
]);
return $defaultBillingNumber;
}
/**
* Extract shipment number from API response
*/
private function extractShipmentNumber(array $response): ?string
{
return data_get($response, 'items.0.shipmentNo')
?? data_get($response, 'shipments.0.shipmentNo')
?? data_get($response, 'shipmentNo')
?? null;
}
/**
* Extract base64 label data from API response
*/
private function extractLabelData(array $response): ?string
{
return data_get($response, 'items.0.label.b64')
?? data_get($response, 'shipments.0.label.b64')
?? data_get($response, 'shipments.0.label')
?? data_get($response, 'label');
}
/**
* Extract routing code from API response
*/
private function extractRoutingCode(array $response): ?string
{
return data_get($response, 'items.0.routingCode')
?? data_get($response, 'shipments.0.routingCode')
?? null;
}
/**
* Create shipment database record
*/
private function createShipmentRecord(array $orderData, array $payload, array $response, ?string $shipmentNumber): DhlShipment
{
return DhlShipment::create([
'order_id' => $orderData['order_id'] ?? null,
'dhl_shipment_no' => $shipmentNumber,
'routing_code' => $this->extractRoutingCode($response),
'type' => 'outbound',
'product_code' => $payload['shipments'][0]['product'],
'billing_number' => $payload['shipments'][0]['billingNumber'],
'weight_kg' => $payload['shipments'][0]['details']['weight']['value'] / 1000,
'status' => 'created',
'label_format' => $payload['shipments'][0]['print']['format'],
'label_path' => null,
'api_response_data' => $response
]);
}
/**
* Save label file to storage and create label record
*/
private function saveLabelFile(DhlShipment $shipment, ?string $labelBase64, string $format): ?string
{
if (!$labelBase64) {
Log::warning('No label data received for shipment', ['shipmentId' => $shipment->id]);
return null;
}
$path = 'dhl/labels/' . $shipment->dhl_shipment_no . '.' . strtolower($format);
$success = false;
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
Storage::disk('local')->put($path, base64_decode($labelBase64));
$success = true;
break;
} catch (\Exception $e) {
Log::warning('Storage put failed, retrying', ['attempt' => $attempt, 'error' => $e->getMessage()]);
usleep(1000000); // 1 second in microseconds
}
}
if (!$success) {
throw new \Exception('Failed to save label after 3 attempts');
}
$shipment->update(['label_path' => $path]);
return $path;
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace Acme\Dhl\Services;
use Acme\Dhl\Support\DhlClient;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Models\DhlTrackingEvent;
use InvalidArgumentException;
use Exception;
use Acme\Dhl\Jobs\SyncTrackingJob;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
/**
* DHL Tracking Service for fetching and managing tracking information
*/
class TrackingService
{
public function __construct(protected DhlClient $client) {}
/**
* Fetch tracking status for a shipment
*
* @param string $trackingNumber DHL tracking number
* @return array Tracking data from DHL API
* @throws InvalidArgumentException When tracking number is empty
* @throws Exception When API request fails
*/
public function fetchStatus(string $trackingNumber): array
{
if (empty($trackingNumber)) {
throw new InvalidArgumentException('Tracking number is required');
}
if (config('dhl.use_queue')) {
SyncTrackingJob::dispatch($trackingNumber);
return ['queued' => true];
}
$response = $this->client->request('get', '/post-tracking/api/shipments', [], [
'trackingNumber' => $trackingNumber
]);
Log::info('Fetched tracking status', ['trackingNumber' => $trackingNumber]);
$events = data_get($response, 'shipments.0.events', []);
$shipment = $this->findOrCreateShipment($trackingNumber);
$this->updateTrackingEvents($shipment, $events);
return $response;
}
/**
* Update tracking status for multiple shipments
*
* @param array $trackingNumbers Array of tracking numbers to update
* @return array Results for each tracking number
*/
public function updateMultipleStatus(array $trackingNumbers): array
{
$results = [];
foreach ($trackingNumbers as $trackingNumber) {
try {
$results[$trackingNumber] = $this->fetchStatus($trackingNumber);
} catch (Exception $e) {
Log::error('Tracking update failed', ['trackingNumber' => $trackingNumber, 'error' => $e->getMessage()]);
$results[$trackingNumber] = ['error' => $e->getMessage()];
}
}
return $results;
}
/**
* Get latest tracking status for a shipment
*
* @param string $trackingNumber DHL tracking number
* @return array|null Latest tracking event or null if not found
*/
public function getLatestStatus(string $trackingNumber): ?array
{
$shipment = DhlShipment::where('dhl_shipment_no', $trackingNumber)->first();
if (!$shipment) {
return null;
}
$latestEvent = $shipment->trackingEvents()
->orderBy('event_time', 'desc')
->first();
return $latestEvent ? $latestEvent->toArray() : null;
}
/**
* Find existing shipment or create new one
*/
private function findOrCreateShipment(string $trackingNumber): DhlShipment
{
return DB::transaction(function () use ($trackingNumber) {
return DhlShipment::firstOrCreate(
['dhl_shipment_no' => $trackingNumber],
[
'type' => 'outbound',
'status' => 'unknown'
]
);
});
}
/**
* Update tracking events for a shipment
*/
private function updateTrackingEvents(DhlShipment $shipment, array $events): void
{
foreach ($events as $eventData) {
$this->createOrUpdateTrackingEvent($shipment, $eventData);
}
// Update shipment status based on latest event
$this->updateShipmentStatus($shipment, $events);
}
/**
* Create or update a tracking event
*/
private function createOrUpdateTrackingEvent(DhlShipment $shipment, array $eventData): void
{
DhlTrackingEvent::updateOrCreate(
[
'shipment_id' => $shipment->id,
'status_code' => $eventData['statusCode'] ?? null,
'event_time' => $eventData['timestamp'] ?? null
],
[
'status_text' => $eventData['status'] ?? null,
'location' => $this->extractLocation($eventData),
'raw' => $eventData
]
);
}
/**
* Extract location from event data
*/
private function extractLocation(array $eventData): ?string
{
return data_get($eventData, 'location.address.addressLocality')
?? data_get($eventData, 'location')
?? null;
}
/**
* Update shipment status based on latest tracking event
*/
private function updateShipmentStatus(DhlShipment $shipment, array $events): void
{
if (empty($events)) {
return;
}
// Sort events by timestamp to get the latest
usort($events, function ($a, $b) {
return strtotime($b['timestamp'] ?? '0') <=> strtotime($a['timestamp'] ?? '0');
});
$latestEvent = $events[0];
$statusCode = $latestEvent['statusCode'] ?? null;
// Map DHL status codes to our internal status
$status = DhlShipment::STATUS_MAP[$statusCode] ?? 'unknown';
$shipment->update([
'status' => $status,
'tracking_status' => $latestEvent['status'] ?? null,
'last_tracked_at' => now()
]);
Log::info('Updated shipment status', ['shipmentId' => $shipment->id, 'newStatus' => $status]);
}
}

View file

@ -0,0 +1,198 @@
<?php
namespace Acme\Dhl\Support;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\ConnectionException;
use Acme\Dhl\Exceptions\DhlApiException;
use Acme\Dhl\Exceptions\DhlAuthenticationException;
use Acme\Dhl\Exceptions\DhlValidationException;
use Exception;
use Illuminate\Support\Facades\Log;
/**
* DHL API Client for handling HTTP requests to DHL services
*/
class DhlClient
{
public function __construct(
protected string $baseUrl,
protected ?string $apiKey,
protected ?string $username,
protected ?string $password
) {}
/**
* Make HTTP request to DHL API
*
* @param string $method HTTP method (get, post, put, delete)
* @param string $uri API endpoint URI
* @param array $payload Request body data
* @param array $query Query parameters
* @return array Response data as array
* @throws Exception When API request fails or returns error
*/
public function request(string $method, string $uri, array $payload = [], array $query = []): array
{
try {
$request = Http::baseUrl($this->baseUrl)
->withHeaders($this->buildHeaders())
->timeout(30)
->retry(3, 300, function ($exception, $attempt) {
if ($exception instanceof RequestException && $exception->response->status() === 429) {
$delay = min(1000000 * $attempt, 10000000); // Max 10 seconds
usleep($delay); // Microseconds
return true;
}
return $exception instanceof ConnectionException ||
($exception instanceof RequestException && in_array($exception->response->status(), [500, 502, 503, 504]));
}, false);
// Add authentication if provided
if ($this->username && $this->password) {
$request = $request->withBasicAuth($this->username, $this->password);
}
// Make the request
$response = match (strtolower($method)) {
'get' => $request->get($uri, $query),
'post' => $request->post($uri . '?' . http_build_query($query), $payload),
'put' => $request->put($uri, $payload),
'delete' => $request->delete($uri),
default => throw new Exception("Unsupported HTTP method: {$method}")
};
// Handle response
if ($response->failed()) {
// Log additional debug info for 400 errors
if ($response->status() === 400) {
Log::error('[DHL API] HTTP 400 Bad Request Details', [
'method' => $method,
'uri' => $uri,
'request_payload' => $payload,
'response_body' => $response->body(),
'response_json' => $response->json(),
'status_code' => $response->status(),
'headers' => $response->headers()
]);
}
$this->handleErrorResponse($response, $method, $uri);
}
return $response->json() ?? [];
} catch (RequestException $e) {
Log::error('DHL API request failed', ['error' => $e->getMessage(), 'method' => $method, 'uri' => $uri]);
throw new DhlApiException("DHL API request failed: {$e->getMessage()}", $e->getCode(), $e);
} catch (Exception $e) {
Log::error('DHL API error', ['error' => $e->getMessage()]);
// Re-throw our own exceptions
if ($e instanceof DhlApiException) {
throw $e;
}
throw new DhlApiException("DHL API error: {$e->getMessage()}", $e->getCode(), $e);
}
}
/**
* Build HTTP headers for DHL API requests
*/
private function buildHeaders(): array
{
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => 'acme-laravel-dhl/1.0'
];
if ($this->apiKey) {
$headers['dhl-api-key'] = $this->apiKey;
}
return $headers;
}
/**
* Handle error responses from DHL API
*/
private function handleErrorResponse($response, string $method, string $uri): void
{
$status = $response->status();
$body = $response->json();
$errorMessage = $this->extractErrorMessage($body) ?? "HTTP {$status} error";
match (true) {
$status === 401 => throw new DhlAuthenticationException("DHL API authentication failed: {$errorMessage}"),
$status === 403 => throw new DhlAuthenticationException("DHL API access forbidden: {$errorMessage}"),
$status === 404 => throw new DhlApiException("DHL API endpoint not found: {$method} {$uri}"),
$status === 422 => throw new DhlValidationException("DHL API validation error: {$errorMessage}"),
$status === 429 => throw new DhlApiException("DHL API rate limit exceeded. Please try again later."),
$status >= 500 => throw new DhlApiException("DHL API server error: {$errorMessage}"),
default => throw new DhlApiException("DHL API error ({$status}): {$errorMessage}")
};
}
/**
* Extract error message from DHL API response
*/
private function extractErrorMessage(?array $body): ?string
{
if (!$body) {
return null;
}
// Try different possible error message fields
return $body['message']
?? $body['error']
?? $body['detail']
?? data_get($body, 'errors.0.message')
?? data_get($body, 'error.message')
?? null;
}
/**
* Test connection to DHL API
*
* @return bool True if connection successful
*/
public function testConnection(): bool
{
try {
// Check basic connectivity and authentication
// Use the simplest possible endpoint to minimize permission issues
$response = Http::baseUrl($this->baseUrl)
->withHeaders($this->buildHeaders())
->withBasicAuth($this->username, $this->password)
->timeout(10)
->get('/');
// If we get any response (even 404), the connection and auth are working
if ($response->status() === 401) {
Log::error('DHL API authentication failed: Invalid username/password');
return false;
}
if ($response->status() === 403 && str_contains($response->body(), 'api-key')) {
Log::error('DHL API authentication failed: Invalid API key');
return false;
}
// Any other response code (including 404, 403 for endpoint access) means connection works
Log::info('DHL API connection test successful', [
'status' => $response->status(),
'has_api_key' => !empty($this->apiKey),
'has_credentials' => !empty($this->username) && !empty($this->password)
]);
return true;
} catch (Exception $e) {
Log::error('DHL API connection test failed', [
'error' => $e->getMessage(),
'base_url' => $this->baseUrl
]);
return false;
}
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use Acme\Dhl\Services\ShippingService;
use ReflectionClass;
use ReflectionMethod;
/**
* Test cases for German address parsing functionality
*/
class AddressParserTest extends TestCase
{
private ShippingService $shippingService;
private ReflectionMethod $parseMethod;
protected function setUp(): void
{
parent::setUp();
// Mock DhlClient for testing
$mockClient = $this->createMock(\Acme\Dhl\Support\DhlClient::class);
$this->shippingService = new ShippingService($mockClient);
// Make private method accessible for testing
$reflection = new ReflectionClass($this->shippingService);
$this->parseMethod = $reflection->getMethod('parseGermanAddress');
$this->parseMethod->setAccessible(true);
}
/**
* Test standard German address formats
*/
public function testStandardGermanAddresses(): void
{
$testCases = [
// Basic format: Street + Number
'Musterstraße 123' => ['street' => 'Musterstraße', 'houseNumber' => '123'],
'Hauptstraße 1' => ['street' => 'Hauptstraße', 'houseNumber' => '1'],
// With letter suffix
'Bahnhofstraße 45a' => ['street' => 'Bahnhofstraße', 'houseNumber' => '45a'],
'Kirchgasse 12B' => ['street' => 'Kirchgasse', 'houseNumber' => '12B'],
// Multi-word street names
'Am Markt 7' => ['street' => 'Am Markt', 'houseNumber' => '7'],
'An der Kirche 23' => ['street' => 'An der Kirche', 'houseNumber' => '23'],
'Karl-Marx-Straße 156' => ['street' => 'Karl-Marx-Straße', 'houseNumber' => '156'],
// Range numbers
'Lindenstraße 1-3' => ['street' => 'Lindenstraße', 'houseNumber' => '1-3'],
'Gartenweg 12/14' => ['street' => 'Gartenweg', 'houseNumber' => '12/14'],
// With abbreviations
'Muster Str. 99' => ['street' => 'Muster Str.', 'houseNumber' => '99'],
'Berliner Pl. 5' => ['street' => 'Berliner Pl.', 'houseNumber' => '5'],
];
foreach ($testCases as $input => $expected) {
$result = $this->parseMethod->invoke($this->shippingService, $input);
$this->assertEquals($expected['street'], $result['street'], "Failed parsing street for: {$input}");
$this->assertEquals($expected['houseNumber'], $result['houseNumber'], "Failed parsing house number for: {$input}");
}
}
/**
* Test edge cases and problematic addresses
*/
public function testEdgeCases(): void
{
$edgeCases = [
// No house number - should return null for houseNumber
'Musterstraße' => ['street' => 'Musterstraße', 'houseNumber' => null],
'Am Markt' => ['street' => 'Am Markt', 'houseNumber' => null],
// Empty string
'' => ['street' => '', 'houseNumber' => null],
// Only number
'123' => ['street' => '123', 'houseNumber' => null],
// Multiple spaces
'Muster Straße 123' => ['street' => 'Muster Straße', 'houseNumber' => '123'],
];
foreach ($edgeCases as $input => $expected) {
$result = $this->parseMethod->invoke($this->shippingService, $input);
$this->assertEquals($expected['street'], $result['street'], "Failed parsing street for edge case: {$input}");
$this->assertEquals($expected['houseNumber'], $result['houseNumber'], "Failed parsing house number for edge case: {$input}");
}
}
/**
* Test international addresses (should not be parsed)
*/
public function testInternationalAddresses(): void
{
$internationalCases = [
// English-style addresses
'123 Main Street' => ['street' => '123 Main Street', 'houseNumber' => null],
'456 Oak Avenue' => ['street' => '456 Oak Avenue', 'houseNumber' => null],
];
foreach ($internationalCases as $input => $expected) {
$result = $this->parseMethod->invoke($this->shippingService, $input);
$this->assertEquals($expected['street'], $result['street'], "International address should not be parsed: {$input}");
$this->assertEquals($expected['houseNumber'], $result['houseNumber'], "International address should have null house number: {$input}");
}
}
}

View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2

View file

@ -0,0 +1 @@
/vendor

View file

@ -0,0 +1,23 @@
# Contributing to laravel-collective-spatie-html-parser
Thank you for considering contributing to this project! Your contributions help maintain and improve this Laravel compatibility layer, ensuring a smooth transition between the obsolete laravel-collective library to the maintained laravel-spatie-html. This library also helps to that old proyects to be updated to the new versions of Laravel.
## How to Contribute
### 1. Submitting a Pull Request (PR)
- Always create your PR against the `develop` branch.
- Use a **descriptive name** and provide a **clear description** of what the PR does (bug fix, new feature, improvement, etc.).
- Once reviewed and approved, I will merge it into `master` and handle the deployment.
### 2. Reporting Issues
If you encounter a bug but are unable to fix it yourself, you can still help by reporting it:
- Open a new **Issue** in the repository.
- Provide a **detailed description** of the issue.
- Include **steps to reproduce the problem** so others can verify and fix it.
## Code Style Guidelines
- Follow Laravels coding standards and best practices.
- Keep the code clean and well-documented.
- Ensure that new features or fixes do not break existing functionality.
Thank you for your support and contributions!

View file

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2024 Christian Albán
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,66 @@
# Laravel Collective to Spatie Laravel HTML Adapter
This package serves as an adapter to help projects that depend on the obsolete `laravel-collective/html` library. It provides an interface that uses the same syntax as the `Form` class of `laravel-collective/html` to create HTML elements. Under the hood, it utilizes the `spatie/laravel-html` library, which is actively maintained, to generate the HTML elements. This allows projects to update to newer Laravel versions without changing the HTML creation syntax.
## Features
- Zero configuration needed, works out of the box.
- Uses the same syntax as `laravel-collective/html`.
- Leverages the actively maintained `spatie/laravel-html` library.
## Available Methods
The following methods can be used and are located in the `src/FormAdapter` directory:
- `checkbox($name, $value = 1, $checked = null, $options = [])`
- `open(array $options = [])`
- `label($name, $value = null, $options = [], $escape_html = true)`
- `text($name, $value = null, $options = [])`
- `password($name, $options = [])`
- `select($name, $list = [], $selected = null, array $selectAttributes = [], array $optionsAttributes = [], array $optgroupsAttributes = [])`
- `radio($name, $value = null, $checked = null, $options = [])`
- `submit($value = null, $options = [])`
- `close()`
- `input($type, $name, $value = null, $options = [])`
- `search($name, $value = null, $options = [])`
- `model($model, array $options = [])`
- `hidden($name, $value = null, $options = [])`
- `email($name, $value = null, $options = [])`
- `tel($name, $value = null, $options = [])`
- `number($name, $value = null, $options = [])`
- `date($name, $value = null, $options = [])`
- `datetime($name, $value = null, $options = [])`
- `datetimeLocal($name, $value = null, $options = [])`
- `time($name, $value = null, $options = [])`
- `url($name, $value = null, $options = [])`
- `file($name, $options = [])`
- `textarea($name, $value = null, $options = [])`
- `reset($value, $attributes = [])`
- `image($url, $name = null, $attributes = [])`
- `color($name, $value = null, $options = [])`
- `button($value = null, $options = [])`
## Installation
To install the package, use composer:
```sh
composer require alban/laravel-collective-spatie-html-parser
```
## Usage
The methods listed above can be used in the same way as you would use the Form class from laravel-collective. Here is an example in a Blade template:
```php
{{-- Using the FormAdapter class in a Blade template --}}
{!! Form::text('relationship', $item->client->agent_relationship, ['required', 'class' => 'form-control input-sm']) !!}
```
For more examples, please refer to the source code in the `src/FormAdapter.php` class file.
## License
This package is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
## Contributing
You are welcome to contribute to this project. Please refer to the [contributing guidelines](CONTRIBUTING.md) for more information.

View file

@ -0,0 +1,39 @@
{
"name": "alban/laravel-collective-spatie-html-parser",
"description": "Adapter class that allows use spatie/laravel-html for old projects that were using the abanondated laravelcollective/html package, and allow to update to new versions of laravel",
"type": "library",
"require": {
"php": "^8.3 | ^8.2 | ^8.0 | ^7.4",
"spatie/laravel-html": "^3.12",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0"
},
"license": "MIT",
"autoload": {
"psr-4": {
"Alban\\LaravelCollectiveSpatieHtmlParser\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Alban\\LaravelCollectiveSpatieHtmlParser\\ServiceProvider"
],
"aliases": {
"Form": "Alban\\LaravelCollectiveSpatieHtmlParser\\FormFacade"
}
}
},
"authors": [
{
"name": "Christian Albán",
"email": "christianalbanctdb1d@gmail.com"
}
],
"minimum-stability": "stable",
"config": {
"allow-plugins": {
"kylekatarnls/update-helper": true
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,218 @@
<?php
namespace Alban\LaravelCollectiveSpatieHtmlParser;
use Alban\LaravelCollectiveSpatieHtmlParser\Traits\AttributesUtils;
class FormAdapter
{
use AttributesUtils;
public function checkbox($name, $value = 1, $checked = null, $options = [])
{
$element = \Html::checkbox($name, $checked, $value);
return $this->mergeOptions($element, $options);
}
public function open(array $options = [])
{
$method = array_key_exists('method', $options) ? $options['method'] : 'POST';
$route = array_key_exists('route', $options) ? $options['route'] : '';
$files = array_key_exists('files', $options) ? $options['files'] : false;
unset($options['method'], $options['route'], $options['files']);
$form = \Html::getFacadeRoot();
if (is_array($route) && count($route)) {
$action = array_shift($route);
$form = $form->form($method, route($action, $route));
} elseif ($route != null && $route != [] && $route != '') {
$form = $form->form($method, route($route));
} else {
$form = $form->form($method);
}
if ($files) {
$form = $form->acceptsFiles();
}
return $this->mergeOptions($form, $options)->open();
}
public function label($name, $value = null, $options = [], $escape_html = true)
{
$element = \Html::label($value, $name);
return $this->mergeOptions($element, $options);
}
public function text($name, $value = null, $options = [])
{
$element = \Html::text($name, $value);
return $this->mergeOptions($element, $options);
}
public function password($name, $options = [])
{
$element = \Html::password($name);
return $this->mergeOptions($element, $options);
}
public function select(
$name,
$list = [],
$selected = null,
array $selectAttributes = [],
array $optionsAttributes = [],
array $optgroupsAttributes = []
) {
if (isset($selectAttributes['multiple']) || in_array('multiple', $selectAttributes)) {
$element = \Html::select($name, $list, $selected)->multiple();
} else {
$element = \Html::select($name, $list, $selected);
}
// $element = \Html::select($name, $list, $selected);
return $this->mergeOptions($element, $selectAttributes);
}
public function radio($name, $value = null, $checked = null, $options = [])
{
$element = \Html::radio($name, $checked, $value);
return $this->mergeOptions($element, $options);
}
public function submit($value = null, $options = [])
{
$element = \Html::submit($value);
return $this->mergeOptions($element, $options);
}
public function close()
{
\Html::endModel();
return \Html::form()->close();
}
public function input($type, $name, $value = null, $options = [])
{
$element = \Html::input($type, $name, $value);
return $this->mergeOptions($element, $options);
}
public function search($name, $value = null, $options = [])
{
return $this->input('search', $name, $value, $options);
}
public function model($model, array $options = [])
{
\Html::model($model);
return $this->open($options);
}
public function hidden($name, $value = null, $options = [])
{
$element = \Html::hidden($name, $value);
return $this->mergeOptions($element, $options);
}
public function email($name, $value = null, $options = [])
{
$element = \Html::email($name, $value);
return $this->mergeOptions($element, $options);
}
public function tel($name, $value = null, $options = [])
{
$element = \Html::tel($name, $value);
return $this->mergeOptions($element, $options);
}
public function number($name, $value = null, $options = [])
{
return $this->input('number', $name, $value, $options);
}
public function date($name, $value = null, $options = [])
{
$element = \Html::date($name, $value);
return $this->mergeOptions($element, $options);
}
public function datetime($name, $value = null, $options = [])
{
return $this->input('datetime', $name, $value, $options);
}
public function datetimeLocal($name, $value = null, $options = [])
{
return $this->input('datetime-local', $name, $value, $options);
}
public function time($name, $value = null, $options = [])
{
$element = \Html::time($name, $value);
return $this->mergeOptions($element, $options);
}
public function url($name, $value = null, $options = [])
{
return $this->input('url', $name, $value, $options);
}
public function file($name, $options = [])
{
$element = \Html::file($name);
return $this->mergeOptions($element, $options);
}
public function textarea($name, $value = null, $options = [])
{
$element = \Html::textarea($name, $value);
return $this->mergeOptions($element, $options);
}
public function reset($value, $attributes = [])
{
$element = \Html::reset($value);
return $this->mergeOptions($element, $attributes);
}
public function image($url, $name = null, $attributes = [])
{
$element = \Html::img($url, $name);
return $this->mergeOptions($element, $attributes);
}
public function color($name, $value = null, $options = [])
{
return $this->input('color', $name, $value, $options);
}
public function button($value = null, $options = [])
{
$element = \Html::button($value);
return $this->mergeOptions($element, $options);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Alban\LaravelCollectiveSpatieHtmlParser;
use Illuminate\Support\Facades\Facade;
/**
* @see \Collective\Html\HtmlBuilder
*/
class FormFacade extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'form';
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Alban\LaravelCollectiveSpatieHtmlParser;
use Spatie\Html\Html;
class HtmlAdapter extends Html {}

View file

@ -0,0 +1,18 @@
<?php
namespace Alban\LaravelCollectiveSpatieHtmlParser;
use Illuminate\Support\Facades\Facade;
class HtmlFacade extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor(): string
{
return 'html';
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Alban\LaravelCollectiveSpatieHtmlParser;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
class ServiceProvider extends BaseServiceProvider
{
/**
* Register bindings in the container.
*
* @return void
*/
public function register()
{
$this->registerFormAdpater();
$this->registerHtmlAdapter();
$this->app->alias('form', FormAdapter::class);
$this->app->alias('html', HtmlAdapter::class);
}
/**
* Register the HTML adapter instance.
*
* @return void
*/
protected function registerFormAdpater()
{
$this->app->singleton('form', function () {
return new FormAdapter();
});
}
/**
* Register the HTML adapter instance.
*
* @return void
*/
protected function registerHtmlAdapter()
{
$this->app->singleton('html', function () {
return new HtmlAdapter();
});
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Alban\LaravelCollectiveSpatieHtmlParser\Traits;
trait AttributesUtils
{
public function mergeOptions($element, $options = [])
{
$newElement = $element;
if (isset($options['class'])) {
$newElement = $newElement->addClass($options['class']);
unset($options['class']);
}
if (isset($options['placeholder'])) {
$newElement = $newElement->placeholder($options['placeholder']);
unset($options['placeholder']);
}
foreach ($options as $key => $value) {
if (!$value) {
continue;
}
if (is_numeric($key)) {
$newElement = $newElement->attribute($value, '');
continue;
}
$newElement = $newElement->attribute($key, $value);
}
return $newElement;
}
}

View file

@ -68,4 +68,6 @@ return array (
'shop' => 'Shop',
'to_shop' => 'Zum Shop',
'marketingplan' => 'Marketingplan',
'dhl_cockpit' => 'DHL Cockpit',
'revenue' => 'Umsatz',
);

Some files were not shown because too many files have changed in this diff Show more