- Öffentliches Widerrufsformular /widerruf mit Bestell-Lookup (throttled) - Bestätigungsmail an Kunde + Weiterleitung an bestellungen@ (config app.withdrawal_mail) - Admin-Übersicht/-Detail unter /admin/withdrawals inkl. Sidenav-Eintrag - Widerruf-Button in Portal-Bestellansicht und Footer-Links - Model OrderWithdrawal + Migration + Factory + Tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
187 lines
6.6 KiB
PHP
187 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Web;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Withdrawal\WithdrawalLookupRequest;
|
|
use App\Http\Requests\Withdrawal\WithdrawalStoreRequest;
|
|
use App\Mail\MailWithdrawalConfirmation;
|
|
use App\Models\OrderWithdrawal;
|
|
use App\Models\ShoppingOrder;
|
|
use App\Services\Shop;
|
|
use App\Services\Util;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
class WithdrawalController extends Controller
|
|
{
|
|
/**
|
|
* Bestelltyp einer B2C-Shop-Bestellung (Verbraucher mit Warenkorb/Checkout).
|
|
*/
|
|
private const SHOP_PAYMENT_FOR = 6;
|
|
|
|
public function create(): View
|
|
{
|
|
return view('web.templates.widerruf', $this->viewData([
|
|
'order' => null,
|
|
'prefill_order' => request('order'),
|
|
'prefill_email' => request('email'),
|
|
]));
|
|
}
|
|
|
|
public function lookup(WithdrawalLookupRequest $request): View|RedirectResponse
|
|
{
|
|
$order = $this->findOrder((string) $request->string('order_number'), (string) $request->string('email'));
|
|
|
|
if (! $order) {
|
|
return back()
|
|
->withInput()
|
|
->with('alert-error', __('withdrawal.not_found'));
|
|
}
|
|
|
|
return view('web.templates.widerruf', $this->viewData([
|
|
'order' => $order,
|
|
'prefill_order' => (string) $request->string('order_number'),
|
|
'prefill_email' => (string) $request->string('email'),
|
|
]));
|
|
}
|
|
|
|
public function store(WithdrawalStoreRequest $request): View|RedirectResponse
|
|
{
|
|
$order = $this->findOrder((string) $request->string('order_number'), (string) $request->string('email'));
|
|
|
|
if (! $order) {
|
|
return redirect()
|
|
->route('withdrawal.create')
|
|
->with('alert-error', __('withdrawal.not_found'));
|
|
}
|
|
|
|
$isPartial = $request->input('scope') === OrderWithdrawal::TYPE_PARTIAL;
|
|
$selectedItems = $this->collectSelectedItems($order, $isPartial ? (array) $request->input('items', []) : []);
|
|
|
|
if ($isPartial && empty($selectedItems)) {
|
|
return back()
|
|
->withInput()
|
|
->with('alert-error', __('withdrawal.no_items_selected'));
|
|
}
|
|
|
|
$withdrawal = OrderWithdrawal::create([
|
|
'shopping_order_id' => $order->id,
|
|
'user_shop_id' => $order->user_shop_id,
|
|
'type' => $isPartial ? OrderWithdrawal::TYPE_PARTIAL : OrderWithdrawal::TYPE_FULL,
|
|
'status' => OrderWithdrawal::STATUS_RECEIVED,
|
|
'order_reference' => (string) $order->id,
|
|
'customer_email' => mb_strtolower((string) $request->string('email')),
|
|
'customer_name' => trim(($order->shopping_user->billing_firstname ?? '').' '.($order->shopping_user->billing_lastname ?? '')) ?: null,
|
|
'reason' => $request->input('reason'),
|
|
'items' => $isPartial ? $selectedItems : $this->collectSelectedItems($order, $this->allItemQuantities($order)),
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => (string) $request->userAgent(),
|
|
'declared_at' => now(),
|
|
]);
|
|
|
|
$this->sendConfirmation($withdrawal, $order);
|
|
|
|
return view('web.templates.widerruf-final', $this->viewData([
|
|
'withdrawal' => $withdrawal,
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Findet eine Verbraucher-Shop-Bestellung anhand der vom Kunden bekannten Nummer
|
|
* (Bestell-ID, Zahlungsreferenz oder Rechnungsnummer) in Kombination mit der E-Mail-Adresse.
|
|
*/
|
|
private function findOrder(string $orderNumber, string $email): ?ShoppingOrder
|
|
{
|
|
$orderNumber = trim($orderNumber);
|
|
$email = mb_strtolower(trim($email));
|
|
|
|
if ($orderNumber === '' || $email === '') {
|
|
return null;
|
|
}
|
|
|
|
return ShoppingOrder::query()
|
|
->where('payment_for', self::SHOP_PAYMENT_FOR)
|
|
->whereHas('shopping_user', function ($query) use ($email): void {
|
|
$query->whereRaw('LOWER(billing_email) = ?', [$email]);
|
|
})
|
|
->where(function ($query) use ($orderNumber): void {
|
|
if (ctype_digit($orderNumber)) {
|
|
$query->orWhere('id', (int) $orderNumber);
|
|
}
|
|
$query->orWhereHas('shopping_payments', function ($payment) use ($orderNumber): void {
|
|
$payment->where('reference', $orderNumber);
|
|
});
|
|
$query->orWhereHas('user_invoices', function ($invoice) use ($orderNumber): void {
|
|
$invoice->where('full_number', $orderNumber);
|
|
});
|
|
})
|
|
->with(['shopping_order_items.product', 'shopping_user'])
|
|
->latest('id')
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, int>
|
|
*/
|
|
private function allItemQuantities(ShoppingOrder $order): array
|
|
{
|
|
return $order->shopping_order_items
|
|
->mapWithKeys(fn ($item): array => [$item->id => (int) $item->qty])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* Erstellt einen Snapshot der widerrufenen Positionen.
|
|
*
|
|
* @param array<int|string, mixed> $quantities
|
|
* @return array<int, array{shopping_order_item_id: int, product_id: int|null, name: string, qty: int}>
|
|
*/
|
|
private function collectSelectedItems(ShoppingOrder $order, array $quantities): array
|
|
{
|
|
$selected = [];
|
|
|
|
foreach ($order->shopping_order_items as $item) {
|
|
$requestedQty = (int) ($quantities[$item->id] ?? 0);
|
|
|
|
if ($requestedQty <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$qty = min($requestedQty, (int) $item->qty);
|
|
|
|
$selected[] = [
|
|
'shopping_order_item_id' => $item->id,
|
|
'product_id' => $item->product_id,
|
|
'name' => $item->product?->getLang('name') ?? ('#'.$item->product_id),
|
|
'qty' => $qty,
|
|
];
|
|
}
|
|
|
|
return $selected;
|
|
}
|
|
|
|
private function sendConfirmation(OrderWithdrawal $withdrawal, ShoppingOrder $order): void
|
|
{
|
|
$bcc = config('app.withdrawal_mail');
|
|
|
|
Mail::to($withdrawal->customer_email)
|
|
->bcc($bcc)
|
|
->locale($order->getLocale())
|
|
->send(new MailWithdrawalConfirmation($withdrawal, $order));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $extra
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function viewData(array $extra = []): array
|
|
{
|
|
return array_merge([
|
|
'user_shop' => Util::getUserShop(),
|
|
'mylangs' => Shop::getLangChange('webshop'),
|
|
'yard_instance' => 'webshop',
|
|
], $extra);
|
|
}
|
|
}
|