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

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
{
@ -170,6 +171,11 @@ class ModalController extends Controller
$user_abo = UserAbo::find($data['id']);
$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'){
$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]);
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');
}
}
\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,41 +342,43 @@ 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()) {
\Session()->flash('shop-name-error', 'error');
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']);
$user_shop = UserShop::create([
'user_id' => $user->id,
'name' => $slug,
'active' => true,
'active_date' => now(),
]
);
$ret = $this->userShopRegisterSubDomain($user_shop->slug);
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,
'active' => true,
'active_date' => now(),
]
);
}
\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,24 +37,26 @@ class DomainResolver
// leiten wir sicher auf die Hauptdomain um.
if ($context->isUnknown()) {
// Detailliertes Logging für spätere Analyse
\Log::warning('Unknown domain accessed', [
'host' => $request->getHost(),
'subdomain' => $context->subdomain,
'user_agent' => $request->userAgent(),
'ip' => $request->ip(),
'referer' => $request->header('referer'),
'path' => $request->getPathInfo()
]);
if(config('app.debug')){
\Log::warning('Unknown domain accessed', [
'host' => $request->getHost(),
'subdomain' => $context->subdomain,
'user_agent' => $request->userAgent(),
'ip' => $request->ip(),
'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);
}
\Log::debug('DomainResolver: context', [
'context' => $context,
'subdomain' => $context->subdomain
]);
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();
}
}

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

@ -16,7 +16,7 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
Schema::defaultStringLength(191);
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
@ -25,7 +25,7 @@ class AppServiceProvider extends ServiceProvider
\View::composer('*', function ($view) {
try {
$context = app(\App\Domain\DomainContext::class);
// Für die Main-Domain: user_shop immer auf null setzen
if ($context->type === 'main') {
$view->with('user_shop', null);
@ -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());
\Log::debug('DomainServiceProvider: domainInfo', [
'domainInfo' => $domainInfo,
'host' => $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,7 +126,9 @@ class DomainService
$subdomain = null;
if (count($parts) > 2) {
$subdomain = $parts[0];
\Log::debug('DomainService: Using extracted subdomain', ['subdomain' => $subdomain, 'host' => $host]);
if (config('app.debug')) {
\Log::debug('DomainService: Using extracted subdomain', ['subdomain' => $subdomain, 'host' => $host]);
}
}
// Determine domain type based on subdomain and host

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;
@ -13,7 +14,7 @@ class Util
{
private static $postRoute = 'base.';
public static function getToken()
{
return hash_hmac('sha256', Str::random(40), config('app.key'));
@ -23,87 +24,95 @@ 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(){
return \App::getLocale() === "en" ? ',' : '.';
public static function _thousands_separator()
{
return \App::getLocale() === "en" ? ',' : '.';
}
public static function _decimal_separator(){
return \App::getLocale() === "en" ? '.' : ',';
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);
// Konvertiere zu Integer und entferne führende Nullen
$number = (int)$cleanStr;
return $number;
}
public static function cleanNumberFormat($num = 0, $dec = 2, $fullzero = false){
if($fullzero && $num == 0){
public static function cleanNumberFormat($num = 0, $dec = 2, $fullzero = false)
{
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;
@ -347,25 +380,61 @@ class Util
return $size;
}
}
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')) ?
mb_strtolower($clean, 'UTF-8') :
strtolower($clean) :
mb_strtolower($clean, 'UTF-8') :
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

@ -129,62 +129,4 @@ if (! function_exists('legal_url')) {
return url($path);
}
}
}
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);
}
}