Widerruf: Gesetzliche Online-Widerrufsfunktion (Formular, Bestätigungsmail, Admin-Verwaltung)
- Ö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>
This commit is contained in:
parent
eac923968d
commit
63c36555a5
24 changed files with 1607 additions and 92 deletions
102
app/Http/Controllers/Admin/OrderWithdrawalController.php
Normal file
102
app/Http/Controllers/Admin/OrderWithdrawalController.php
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\OrderWithdrawal;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class OrderWithdrawalController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.withdrawal.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detail(int $id): View
|
||||||
|
{
|
||||||
|
$withdrawal = OrderWithdrawal::with('shopping_order.shopping_user', 'user_shop')->findOrFail($id);
|
||||||
|
|
||||||
|
return view('admin.withdrawal.detail', [
|
||||||
|
'withdrawal' => $withdrawal,
|
||||||
|
'statuses' => OrderWithdrawal::statuses(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $id): RedirectResponse
|
||||||
|
{
|
||||||
|
$withdrawal = OrderWithdrawal::findOrFail($id);
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'status' => ['required', 'in:'.implode(',', OrderWithdrawal::statuses())],
|
||||||
|
'admin_note' => ['nullable', 'string', 'max:5000'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return back()->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$withdrawal->update([
|
||||||
|
'status' => $request->input('status'),
|
||||||
|
'admin_note' => $request->input('admin_note'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
\Session()->flash('alert-success', __('Gespeichert!'));
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function datatable()
|
||||||
|
{
|
||||||
|
$query = OrderWithdrawal::query()->select('order_withdrawals.*');
|
||||||
|
|
||||||
|
return \DataTables::eloquent($query)
|
||||||
|
->addColumn('id', function (OrderWithdrawal $w): string {
|
||||||
|
return '<a href="'.route('admin_withdrawals_detail', [$w->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-eye"></span></a> '.$w->id;
|
||||||
|
})
|
||||||
|
->addColumn('declared_at', function (OrderWithdrawal $w): string {
|
||||||
|
return $w->declared_at ? $w->declared_at->format('d.m.Y H:i') : '';
|
||||||
|
})
|
||||||
|
->addColumn('order_reference', function (OrderWithdrawal $w): string {
|
||||||
|
return '#'.e($w->order_reference);
|
||||||
|
})
|
||||||
|
->addColumn('customer', function (OrderWithdrawal $w): string {
|
||||||
|
return e($w->customer_name).'<br><small>'.e($w->customer_email).'</small>';
|
||||||
|
})
|
||||||
|
->addColumn('type', function (OrderWithdrawal $w): string {
|
||||||
|
$label = $w->isFullWithdrawal() ? __('withdrawal.scope_full') : __('withdrawal.scope_partial');
|
||||||
|
$color = $w->isFullWithdrawal() ? 'dark' : 'info';
|
||||||
|
|
||||||
|
return '<span class="badge badge-pill badge-'.$color.'">'.e($label).'</span>';
|
||||||
|
})
|
||||||
|
->addColumn('status', function (OrderWithdrawal $w): string {
|
||||||
|
$colors = [
|
||||||
|
OrderWithdrawal::STATUS_RECEIVED => 'warning',
|
||||||
|
OrderWithdrawal::STATUS_PROCESSING => 'info',
|
||||||
|
OrderWithdrawal::STATUS_DONE => 'success',
|
||||||
|
OrderWithdrawal::STATUS_REJECTED => 'danger',
|
||||||
|
];
|
||||||
|
$color = $colors[$w->status] ?? 'default';
|
||||||
|
|
||||||
|
return '<span class="badge badge-pill badge-'.$color.'">'.e($w->status).'</span>';
|
||||||
|
})
|
||||||
|
->addColumn('action', function (OrderWithdrawal $w): string {
|
||||||
|
return '<a href="'.route('admin_withdrawals_detail', [$w->id]).'" class="btn btn-sm btn-primary"><i class="fa fa-eye"></i></a>';
|
||||||
|
})
|
||||||
|
->filterColumn('customer', function ($query, $keyword): void {
|
||||||
|
$query->where('customer_email', 'LIKE', '%'.$keyword.'%')
|
||||||
|
->orWhere('customer_name', 'LIKE', '%'.$keyword.'%');
|
||||||
|
})
|
||||||
|
->orderColumn('declared_at', 'declared_at $1')
|
||||||
|
->rawColumns(['id', 'customer', 'type', 'status', 'action'])
|
||||||
|
->make(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
187
app/Http/Controllers/Web/WithdrawalController.php
Normal file
187
app/Http/Controllers/Web/WithdrawalController.php
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Http/Requests/Withdrawal/WithdrawalLookupRequest.php
Normal file
36
app/Http/Requests/Withdrawal/WithdrawalLookupRequest.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Withdrawal;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class WithdrawalLookupRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, mixed>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'order_number' => ['required', 'string', 'max:50'],
|
||||||
|
'email' => ['required', 'email', 'max:255'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'order_number.required' => __('validation.required', ['attribute' => __('withdrawal.order_number')]),
|
||||||
|
'email.required' => __('validation.required', ['attribute' => __('withdrawal.email')]),
|
||||||
|
'email.email' => __('validation.email', ['attribute' => __('withdrawal.email')]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Requests/Withdrawal/WithdrawalStoreRequest.php
Normal file
44
app/Http/Requests/Withdrawal/WithdrawalStoreRequest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Withdrawal;
|
||||||
|
|
||||||
|
use App\Models\OrderWithdrawal;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class WithdrawalStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, mixed>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'order_number' => ['required', 'string', 'max:50'],
|
||||||
|
'email' => ['required', 'email', 'max:255'],
|
||||||
|
'scope' => ['required', Rule::in([OrderWithdrawal::TYPE_FULL, OrderWithdrawal::TYPE_PARTIAL])],
|
||||||
|
'items' => ['array'],
|
||||||
|
'items.*' => ['integer', 'min:0'],
|
||||||
|
'reason' => ['nullable', 'string', 'max:2000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'order_number.required' => __('validation.required', ['attribute' => __('withdrawal.order_number')]),
|
||||||
|
'email.required' => __('validation.required', ['attribute' => __('withdrawal.email')]),
|
||||||
|
'email.email' => __('validation.email', ['attribute' => __('withdrawal.email')]),
|
||||||
|
'scope.required' => __('validation.required', ['attribute' => __('withdrawal.choose_scope_hl')]),
|
||||||
|
'scope.in' => __('validation.required', ['attribute' => __('withdrawal.choose_scope_hl')]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Mail/MailWithdrawalConfirmation.php
Normal file
31
app/Mail/MailWithdrawalConfirmation.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\OrderWithdrawal;
|
||||||
|
use App\Models\ShoppingOrder;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class MailWithdrawalConfirmation extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $subject;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public OrderWithdrawal $withdrawal,
|
||||||
|
public ?ShoppingOrder $shopping_order = null
|
||||||
|
) {
|
||||||
|
$this->subject = __('withdrawal.mail_subject').' - mivita.care';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function build(): self
|
||||||
|
{
|
||||||
|
return $this->view('emails.withdrawal_confirmation')->with([
|
||||||
|
'withdrawal' => $this->withdrawal,
|
||||||
|
'shopping_order' => $this->shopping_order,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/Models/OrderWithdrawal.php
Normal file
107
app/Models/OrderWithdrawal.php
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\OrderWithdrawalFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Online-Widerruf einer Verbraucherbestellung (gesetzliche Widerrufs-Button-Pflicht).
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int|null $shopping_order_id
|
||||||
|
* @property int|null $user_shop_id
|
||||||
|
* @property string $type
|
||||||
|
* @property string $status
|
||||||
|
* @property string|null $order_reference
|
||||||
|
* @property string $customer_email
|
||||||
|
* @property string|null $customer_name
|
||||||
|
* @property string|null $reason
|
||||||
|
* @property array|null $items
|
||||||
|
* @property string|null $ip_address
|
||||||
|
* @property string|null $user_agent
|
||||||
|
* @property string|null $admin_note
|
||||||
|
* @property Carbon|null $declared_at
|
||||||
|
*/
|
||||||
|
class OrderWithdrawal extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<OrderWithdrawalFactory> */
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
public const TYPE_FULL = 'full';
|
||||||
|
|
||||||
|
public const TYPE_PARTIAL = 'partial';
|
||||||
|
|
||||||
|
public const STATUS_RECEIVED = 'received';
|
||||||
|
|
||||||
|
public const STATUS_PROCESSING = 'processing';
|
||||||
|
|
||||||
|
public const STATUS_DONE = 'done';
|
||||||
|
|
||||||
|
public const STATUS_REJECTED = 'rejected';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'shopping_order_id',
|
||||||
|
'user_shop_id',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'order_reference',
|
||||||
|
'customer_email',
|
||||||
|
'customer_name',
|
||||||
|
'reason',
|
||||||
|
'items',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
'admin_note',
|
||||||
|
'declared_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'items' => 'array',
|
||||||
|
'declared_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shopping_order(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ShoppingOrder::class, 'shopping_order_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user_shop(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserShop::class, 'user_shop_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeReceived(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('status', self::STATUS_RECEIVED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function statuses(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STATUS_RECEIVED,
|
||||||
|
self::STATUS_PROCESSING,
|
||||||
|
self::STATUS_DONE,
|
||||||
|
self::STATUS_REJECTED,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFullWithdrawal(): bool
|
||||||
|
{
|
||||||
|
return $this->type === self::TYPE_FULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
238
config/app.php
238
config/app.php
|
|
@ -1,5 +1,80 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Acme\Dhl\DhlServiceProvider;
|
||||||
|
use Acme\Dhl\Facades\DHL;
|
||||||
|
use Alban\LaravelCollectiveSpatieHtmlParser\FormFacade;
|
||||||
|
use Alban\LaravelCollectiveSpatieHtmlParser\HtmlFacade;
|
||||||
|
use Alban\LaravelCollectiveSpatieHtmlParser\ServiceProvider;
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use App\Providers\EventServiceProvider;
|
||||||
|
use App\Providers\HorizonServiceProvider;
|
||||||
|
use App\Providers\RouteServiceProvider;
|
||||||
|
use App\Providers\YardServiceProvider;
|
||||||
|
use App\Services\Facade\Yard;
|
||||||
|
use App\Services\HTMLHelper;
|
||||||
|
use App\Services\Util;
|
||||||
|
use Illuminate\Auth\AuthServiceProvider;
|
||||||
|
use Illuminate\Auth\Passwords\PasswordResetServiceProvider;
|
||||||
|
use Illuminate\Broadcasting\BroadcastServiceProvider;
|
||||||
|
use Illuminate\Bus\BusServiceProvider;
|
||||||
|
use Illuminate\Cache\CacheServiceProvider;
|
||||||
|
use Illuminate\Cookie\CookieServiceProvider;
|
||||||
|
use Illuminate\Database\DatabaseServiceProvider;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Encryption\EncryptionServiceProvider;
|
||||||
|
use Illuminate\Filesystem\FilesystemServiceProvider;
|
||||||
|
use Illuminate\Foundation\Providers\ConsoleSupportServiceProvider;
|
||||||
|
use Illuminate\Foundation\Providers\FoundationServiceProvider;
|
||||||
|
use Illuminate\Hashing\HashServiceProvider;
|
||||||
|
use Illuminate\Mail\MailServiceProvider;
|
||||||
|
use Illuminate\Notifications\NotificationServiceProvider;
|
||||||
|
use Illuminate\Pagination\PaginationServiceProvider;
|
||||||
|
use Illuminate\Pipeline\PipelineServiceProvider;
|
||||||
|
use Illuminate\Queue\QueueServiceProvider;
|
||||||
|
use Illuminate\Redis\RedisServiceProvider;
|
||||||
|
use Illuminate\Session\SessionServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\Cookie;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Lang;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
use Illuminate\Support\Facades\Redirect;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Illuminate\Support\Facades\Request;
|
||||||
|
use Illuminate\Support\Facades\Response;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Illuminate\Translation\TranslationServiceProvider;
|
||||||
|
use Illuminate\Validation\ValidationServiceProvider;
|
||||||
|
use Illuminate\View\ViewServiceProvider;
|
||||||
|
use Jenssegers\Date\Date;
|
||||||
|
use Jenssegers\Date\DateServiceProvider;
|
||||||
|
use Laravel\Tinker\TinkerServiceProvider;
|
||||||
|
use Maatwebsite\Excel\ExcelServiceProvider;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use Yajra\DataTables\DataTablesServiceProvider;
|
||||||
|
use Yajra\DataTables\Facades\DataTables;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -64,6 +139,7 @@ return [
|
||||||
|
|
||||||
'checkout_url' => env('APP_URL_CHECKOUT', 'checkout.'),
|
'checkout_url' => env('APP_URL_CHECKOUT', 'checkout.'),
|
||||||
'contact_mail' => env('APP_CONTACT_MAIL', 'kontakt@mivita.care'),
|
'contact_mail' => env('APP_CONTACT_MAIL', 'kontakt@mivita.care'),
|
||||||
|
'withdrawal_mail' => env('APP_WITHDRAWAL_MAIL', 'bestellungen@mivita.care'),
|
||||||
'checkout_mail' => env('APP_CHECKOUT_MAIL', 'no-reply@mivita.care'),
|
'checkout_mail' => env('APP_CHECKOUT_MAIL', 'no-reply@mivita.care'),
|
||||||
'checkout_test_mail' => env('APP_CHECKOUT_TEST_MAIL', 'devtest@mivita.care'),
|
'checkout_test_mail' => env('APP_CHECKOUT_TEST_MAIL', 'devtest@mivita.care'),
|
||||||
'info_mail' => env('APP_INFO_MAIL', 'no-reply@mivita.care'),
|
'info_mail' => env('APP_INFO_MAIL', 'no-reply@mivita.care'),
|
||||||
|
|
@ -74,7 +150,6 @@ return [
|
||||||
|
|
||||||
'ipinfo' => env('APP_IPINFO', false),
|
'ipinfo' => env('APP_IPINFO', false),
|
||||||
|
|
||||||
|
|
||||||
'main_tax' => env('APP_MAIN_TAX', 1.19),
|
'main_tax' => env('APP_MAIN_TAX', 1.19),
|
||||||
'main_tax_rate' => env('APP_MAIN_TAX_RATE', 19),
|
'main_tax_rate' => env('APP_MAIN_TAX_RATE', 19),
|
||||||
'shipping_tax' => env('APP_SHIPPING_TAX', 19),
|
'shipping_tax' => env('APP_SHIPPING_TAX', 19),
|
||||||
|
|
@ -86,7 +161,6 @@ return [
|
||||||
'url_backend' => env('APP_URL', 'http://mivita.local/'),
|
'url_backend' => env('APP_URL', 'http://mivita.local/'),
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Timezone
|
| Application Timezone
|
||||||
|
|
@ -157,53 +231,53 @@ return [
|
||||||
/*
|
/*
|
||||||
* Laravel Framework Service Providers...
|
* Laravel Framework Service Providers...
|
||||||
*/
|
*/
|
||||||
Illuminate\Auth\AuthServiceProvider::class,
|
AuthServiceProvider::class,
|
||||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
BroadcastServiceProvider::class,
|
||||||
Illuminate\Bus\BusServiceProvider::class,
|
BusServiceProvider::class,
|
||||||
Illuminate\Cache\CacheServiceProvider::class,
|
CacheServiceProvider::class,
|
||||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
ConsoleSupportServiceProvider::class,
|
||||||
Illuminate\Cookie\CookieServiceProvider::class,
|
CookieServiceProvider::class,
|
||||||
Illuminate\Database\DatabaseServiceProvider::class,
|
DatabaseServiceProvider::class,
|
||||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
EncryptionServiceProvider::class,
|
||||||
Illuminate\Filesystem\FilesystemServiceProvider::class,
|
FilesystemServiceProvider::class,
|
||||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
FoundationServiceProvider::class,
|
||||||
Illuminate\Hashing\HashServiceProvider::class,
|
HashServiceProvider::class,
|
||||||
Illuminate\Mail\MailServiceProvider::class,
|
MailServiceProvider::class,
|
||||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
NotificationServiceProvider::class,
|
||||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
PaginationServiceProvider::class,
|
||||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
PipelineServiceProvider::class,
|
||||||
Illuminate\Queue\QueueServiceProvider::class,
|
QueueServiceProvider::class,
|
||||||
Illuminate\Redis\RedisServiceProvider::class,
|
RedisServiceProvider::class,
|
||||||
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
|
PasswordResetServiceProvider::class,
|
||||||
Illuminate\Session\SessionServiceProvider::class,
|
SessionServiceProvider::class,
|
||||||
Illuminate\Translation\TranslationServiceProvider::class,
|
TranslationServiceProvider::class,
|
||||||
Illuminate\Validation\ValidationServiceProvider::class,
|
ValidationServiceProvider::class,
|
||||||
Illuminate\View\ViewServiceProvider::class,
|
ViewServiceProvider::class,
|
||||||
//Reliese\Coders\CodersServiceProvider::class,
|
// Reliese\Coders\CodersServiceProvider::class,
|
||||||
//Intervention\Image\ImageServiceProvider::class,
|
// Intervention\Image\ImageServiceProvider::class,
|
||||||
/*
|
/*
|
||||||
* Package Service Providers...
|
* Package Service Providers...
|
||||||
*/
|
*/
|
||||||
Laravel\Tinker\TinkerServiceProvider::class,
|
TinkerServiceProvider::class,
|
||||||
/*
|
/*
|
||||||
* DHL Package Service Provider...
|
* DHL Package Service Provider...
|
||||||
*/
|
*/
|
||||||
Acme\Dhl\DhlServiceProvider::class,
|
DhlServiceProvider::class,
|
||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
*/
|
*/
|
||||||
App\Providers\AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
App\Providers\AuthServiceProvider::class,
|
App\Providers\AuthServiceProvider::class,
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
// App\Providers\BroadcastServiceProvider::class,
|
||||||
App\Providers\EventServiceProvider::class,
|
EventServiceProvider::class,
|
||||||
App\Providers\HorizonServiceProvider::class,
|
HorizonServiceProvider::class,
|
||||||
//App\Providers\DomainServiceProvider::class,
|
// App\Providers\DomainServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
RouteServiceProvider::class,
|
||||||
Jenssegers\Date\DateServiceProvider::class,
|
DateServiceProvider::class,
|
||||||
Maatwebsite\Excel\ExcelServiceProvider::class,
|
ExcelServiceProvider::class,
|
||||||
Yajra\DataTables\DataTablesServiceProvider::class,
|
DataTablesServiceProvider::class,
|
||||||
App\Providers\YardServiceProvider::class,
|
YardServiceProvider::class,
|
||||||
Alban\LaravelCollectiveSpatieHtmlParser\ServiceProvider::class,
|
ServiceProvider::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -220,50 +294,50 @@ return [
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
|
|
||||||
'App' => Illuminate\Support\Facades\App::class,
|
'App' => Illuminate\Support\Facades\App::class,
|
||||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
'Artisan' => Artisan::class,
|
||||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
'Auth' => Auth::class,
|
||||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
'Blade' => Blade::class,
|
||||||
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
|
'Broadcast' => Broadcast::class,
|
||||||
'Bus' => Illuminate\Support\Facades\Bus::class,
|
'Bus' => Bus::class,
|
||||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
'Cache' => Cache::class,
|
||||||
'Config' => Illuminate\Support\Facades\Config::class,
|
'Config' => Config::class,
|
||||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
'Cookie' => Cookie::class,
|
||||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
'Crypt' => Crypt::class,
|
||||||
'DB' => Illuminate\Support\Facades\DB::class,
|
'DB' => DB::class,
|
||||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
'Eloquent' => Model::class,
|
||||||
'Event' => Illuminate\Support\Facades\Event::class,
|
'Event' => Event::class,
|
||||||
'File' => Illuminate\Support\Facades\File::class,
|
'File' => File::class,
|
||||||
'Gate' => Illuminate\Support\Facades\Gate::class,
|
'Gate' => Gate::class,
|
||||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
'Hash' => Hash::class,
|
||||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
'Lang' => Lang::class,
|
||||||
'Log' => Illuminate\Support\Facades\Log::class,
|
'Log' => Log::class,
|
||||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
'Mail' => Mail::class,
|
||||||
'Notification' => Illuminate\Support\Facades\Notification::class,
|
'Notification' => Notification::class,
|
||||||
'Password' => Illuminate\Support\Facades\Password::class,
|
'Password' => Password::class,
|
||||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
'Queue' => Queue::class,
|
||||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
'Redirect' => Redirect::class,
|
||||||
'Redis' => Illuminate\Support\Facades\Redis::class,
|
'Redis' => Redis::class,
|
||||||
'Request' => Illuminate\Support\Facades\Request::class,
|
'Request' => Request::class,
|
||||||
'Response' => Illuminate\Support\Facades\Response::class,
|
'Response' => Response::class,
|
||||||
'Route' => Illuminate\Support\Facades\Route::class,
|
'Route' => Route::class,
|
||||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
'Schema' => Schema::class,
|
||||||
'Session' => Illuminate\Support\Facades\Session::class,
|
'Session' => Session::class,
|
||||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
'Storage' => Storage::class,
|
||||||
'URL' => Illuminate\Support\Facades\URL::class,
|
'URL' => URL::class,
|
||||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
'Validator' => Validator::class,
|
||||||
'View' => Illuminate\Support\Facades\View::class,
|
'View' => View::class,
|
||||||
//'Input' => Illuminate\Support\Facades\Input::class,
|
// 'Input' => Illuminate\Support\Facades\Input::class,
|
||||||
//'Image' => Intervention\Image\Facades\Image::class,
|
// 'Image' => Intervention\Image\Facades\Image::class,
|
||||||
'Carbon' => Carbon\Carbon::class,
|
'Carbon' => Carbon\Carbon::class,
|
||||||
'Date' => Jenssegers\Date\Date::class,
|
'Date' => Date::class,
|
||||||
'HTMLHelper' => App\Services\HTMLHelper::class,
|
'HTMLHelper' => HTMLHelper::class,
|
||||||
'Util' => App\Services\Util::class,
|
'Util' => Util::class,
|
||||||
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
|
'Excel' => Excel::class,
|
||||||
'DataTables' => Yajra\DataTables\Facades\DataTables::class,
|
'DataTables' => DataTables::class,
|
||||||
'Yard' => App\Services\Facade\Yard::class,
|
'Yard' => Yard::class,
|
||||||
'DHL' => Acme\Dhl\Facades\DHL::class,
|
'DHL' => DHL::class,
|
||||||
'Form' => Alban\LaravelCollectiveSpatieHtmlParser\FormFacade::class,
|
'Form' => FormFacade::class,
|
||||||
'Html' => Alban\LaravelCollectiveSpatieHtmlParser\HtmlFacade::class,
|
'Html' => HtmlFacade::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
40
database/factories/OrderWithdrawalFactory.php
Normal file
40
database/factories/OrderWithdrawalFactory.php
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\OrderWithdrawal;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<OrderWithdrawal>
|
||||||
|
*/
|
||||||
|
class OrderWithdrawalFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'shopping_order_id' => null,
|
||||||
|
'user_shop_id' => null,
|
||||||
|
'type' => OrderWithdrawal::TYPE_FULL,
|
||||||
|
'status' => OrderWithdrawal::STATUS_RECEIVED,
|
||||||
|
'order_reference' => (string) $this->faker->numberBetween(1000, 99999),
|
||||||
|
'customer_email' => $this->faker->safeEmail(),
|
||||||
|
'customer_name' => $this->faker->name(),
|
||||||
|
'reason' => $this->faker->optional()->sentence(),
|
||||||
|
'items' => null,
|
||||||
|
'ip_address' => $this->faker->ipv4(),
|
||||||
|
'user_agent' => $this->faker->userAgent(),
|
||||||
|
'declared_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function partial(): self
|
||||||
|
{
|
||||||
|
return $this->state(fn (): array => ['type' => OrderWithdrawal::TYPE_PARTIAL]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('order_withdrawals', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('shopping_order_id')->nullable()->index();
|
||||||
|
$table->unsignedBigInteger('user_shop_id')->nullable()->index();
|
||||||
|
$table->string('type')->default('full');
|
||||||
|
$table->string('status')->default('received')->index();
|
||||||
|
$table->string('order_reference')->nullable();
|
||||||
|
$table->string('customer_email')->index();
|
||||||
|
$table->string('customer_name')->nullable();
|
||||||
|
$table->text('reason')->nullable();
|
||||||
|
$table->json('items')->nullable();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->text('admin_note')->nullable();
|
||||||
|
$table->timestamp('declared_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('order_withdrawals');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -68,6 +68,7 @@ return [
|
||||||
'customerabo' => 'Kunden Abo',
|
'customerabo' => 'Kunden Abo',
|
||||||
'abo' => 'Abo',
|
'abo' => 'Abo',
|
||||||
'abos' => 'Abos',
|
'abos' => 'Abos',
|
||||||
|
'withdrawals' => 'Widerrufe',
|
||||||
'payment_links' => 'Bezahllinks',
|
'payment_links' => 'Bezahllinks',
|
||||||
'dashboard' => 'Dashboard',
|
'dashboard' => 'Dashboard',
|
||||||
'shop' => 'Shop',
|
'shop' => 'Shop',
|
||||||
|
|
|
||||||
63
resources/lang/de/withdrawal.php
Normal file
63
resources/lang/de/withdrawal.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
// Buttons / Navigation
|
||||||
|
'button_label' => 'Vertrag widerrufen',
|
||||||
|
'nav_title' => 'Widerruf',
|
||||||
|
|
||||||
|
// Identifikationsformular
|
||||||
|
'page_title' => 'Vertrag widerrufen',
|
||||||
|
'intro' => 'Hier kannst du deine online abgeschlossene Bestellung ganz einfach widerrufen. Bitte identifiziere zunächst deine Bestellung.',
|
||||||
|
'identify_hl' => 'Bestellung identifizieren',
|
||||||
|
'order_number' => 'Bestellnummer',
|
||||||
|
'order_number_placeholder' => 'z. B. 12345',
|
||||||
|
'order_number_help' => 'Die Bestellnummer findest du in deiner Bestellbestätigung.',
|
||||||
|
'email' => 'E-Mail-Adresse der Bestellung',
|
||||||
|
'email_placeholder' => 'deine@email.de',
|
||||||
|
'find_order' => 'Bestellung suchen',
|
||||||
|
|
||||||
|
// Bestellung gefunden / Auswahl
|
||||||
|
'order_found_hl' => 'Deine Bestellung',
|
||||||
|
'order_date' => 'Bestelldatum',
|
||||||
|
'choose_scope_hl' => 'Was möchtest du widerrufen?',
|
||||||
|
'scope_full' => 'Vollständiger Widerruf (gesamte Bestellung)',
|
||||||
|
'scope_partial' => 'Teilweiser Widerruf (einzelne Artikel)',
|
||||||
|
'select_items_hl' => 'Artikel auswählen',
|
||||||
|
'product' => 'Artikel',
|
||||||
|
'quantity' => 'Menge',
|
||||||
|
'withdraw_quantity' => 'Zu widerrufende Menge',
|
||||||
|
'reason' => 'Grund (optional)',
|
||||||
|
'reason_placeholder' => 'Du musst keinen Grund angeben. Falls du möchtest, kannst du uns hier deinen Grund mitteilen.',
|
||||||
|
'submit' => 'Widerruf verbindlich absenden',
|
||||||
|
'back' => 'Zurück',
|
||||||
|
'legal_note' => 'Mit dem Absenden erklärst du den Widerruf deines Vertrags gegenüber der mivita care gmbh, Leinfeld 2, 87755 Kirchhaslach. Du erhältst umgehend eine Eingangsbestätigung per E-Mail mit Datum und Uhrzeit.',
|
||||||
|
|
||||||
|
// Fehler
|
||||||
|
'not_found' => 'Wir konnten keine passende Bestellung zu diesen Angaben finden. Bitte prüfe Bestellnummer und E-Mail-Adresse.',
|
||||||
|
'no_items_selected' => 'Bitte wähle mindestens einen Artikel für den teilweisen Widerruf aus.',
|
||||||
|
|
||||||
|
// Erfolg / Eingangsbestätigung
|
||||||
|
'success_hl' => 'Dein Widerruf ist bei uns eingegangen',
|
||||||
|
'success_intro' => 'Vielen Dank. Wir haben deinen Widerruf erhalten und bearbeiten ihn schnellstmöglich.',
|
||||||
|
'clock' => 'Uhr',
|
||||||
|
'success_received_at' => 'Eingang deines Widerrufs',
|
||||||
|
'success_reference' => 'Vorgangsnummer',
|
||||||
|
'success_email_sent' => 'Eine Eingangsbestätigung haben wir zusätzlich an :email gesendet.',
|
||||||
|
'success_full' => 'Du hast die gesamte Bestellung widerrufen.',
|
||||||
|
'success_partial' => 'Du hast einzelne Artikel deiner Bestellung widerrufen.',
|
||||||
|
|
||||||
|
// E-Mail
|
||||||
|
'mail_subject' => 'Eingangsbestätigung deines Widerrufs',
|
||||||
|
'mail_hello' => 'Hallo',
|
||||||
|
'mail_intro' => 'wir bestätigen den Eingang deines Widerrufs zu folgender Bestellung. Dein Widerruf ist zum genannten Zeitpunkt bei uns eingegangen:',
|
||||||
|
'mail_received_at' => 'Eingegangen am',
|
||||||
|
'mail_order_number' => 'Bestellnummer',
|
||||||
|
'mail_reference' => 'Vorgangsnummer',
|
||||||
|
'mail_scope' => 'Umfang des Widerrufs',
|
||||||
|
'mail_items_hl' => 'Widerrufene Artikel',
|
||||||
|
'mail_outro' => 'Wir kümmern uns um die weitere Abwicklung und melden uns bei Bedarf bei dir.',
|
||||||
|
'mail_greetings' => 'Beste Grüße',
|
||||||
|
'mail_sender' => 'Dein mivita.care Team',
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
return [
|
return [
|
||||||
'abo' => 'Subscription',
|
'abo' => 'Subscription',
|
||||||
'abos' => 'Subscriptions',
|
'abos' => 'Subscriptions',
|
||||||
|
'withdrawals' => 'Withdrawals',
|
||||||
'add' => 'add',
|
'add' => 'add',
|
||||||
'attribute' => 'attributes',
|
'attribute' => 'attributes',
|
||||||
'business' => 'business',
|
'business' => 'business',
|
||||||
|
|
|
||||||
63
resources/lang/en/withdrawal.php
Normal file
63
resources/lang/en/withdrawal.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
// Buttons / Navigation
|
||||||
|
'button_label' => 'Withdraw contract',
|
||||||
|
'nav_title' => 'Withdrawals',
|
||||||
|
|
||||||
|
// Identification form
|
||||||
|
'page_title' => 'Withdraw your contract',
|
||||||
|
'intro' => 'Here you can easily withdraw the contract for an order you concluded online. Please identify your order first.',
|
||||||
|
'identify_hl' => 'Identify your order',
|
||||||
|
'order_number' => 'Order number',
|
||||||
|
'order_number_placeholder' => 'e.g. 12345',
|
||||||
|
'order_number_help' => 'You can find the order number in your order confirmation.',
|
||||||
|
'email' => 'E-mail address of the order',
|
||||||
|
'email_placeholder' => 'your@email.com',
|
||||||
|
'find_order' => 'Find order',
|
||||||
|
|
||||||
|
// Order found / selection
|
||||||
|
'order_found_hl' => 'Your order',
|
||||||
|
'order_date' => 'Order date',
|
||||||
|
'choose_scope_hl' => 'What would you like to withdraw?',
|
||||||
|
'scope_full' => 'Full withdrawal (entire order)',
|
||||||
|
'scope_partial' => 'Partial withdrawal (individual items)',
|
||||||
|
'select_items_hl' => 'Select items',
|
||||||
|
'product' => 'Item',
|
||||||
|
'quantity' => 'Quantity',
|
||||||
|
'withdraw_quantity' => 'Quantity to withdraw',
|
||||||
|
'reason' => 'Reason (optional)',
|
||||||
|
'reason_placeholder' => 'You do not have to provide a reason. If you like, you can let us know your reason here.',
|
||||||
|
'submit' => 'Submit withdrawal',
|
||||||
|
'back' => 'Back',
|
||||||
|
'legal_note' => 'By submitting, you declare the withdrawal of your contract with mivita care gmbh, Leinfeld 2, 87755 Kirchhaslach, Germany. You will immediately receive a confirmation of receipt by e-mail including date and time.',
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
'not_found' => 'We could not find a matching order for these details. Please check the order number and e-mail address.',
|
||||||
|
'no_items_selected' => 'Please select at least one item for the partial withdrawal.',
|
||||||
|
|
||||||
|
// Success / confirmation
|
||||||
|
'success_hl' => 'We have received your withdrawal',
|
||||||
|
'success_intro' => 'Thank you. We have received your withdrawal and will process it as soon as possible.',
|
||||||
|
'clock' => '',
|
||||||
|
'success_received_at' => 'Receipt of your withdrawal',
|
||||||
|
'success_reference' => 'Reference number',
|
||||||
|
'success_email_sent' => 'We have also sent a confirmation of receipt to :email.',
|
||||||
|
'success_full' => 'You have withdrawn the entire order.',
|
||||||
|
'success_partial' => 'You have withdrawn individual items of your order.',
|
||||||
|
|
||||||
|
// E-mail
|
||||||
|
'mail_subject' => 'Confirmation of receipt of your withdrawal',
|
||||||
|
'mail_hello' => 'Hello',
|
||||||
|
'mail_intro' => 'we confirm receipt of your withdrawal for the following order. Your withdrawal was received by us at the time stated:',
|
||||||
|
'mail_received_at' => 'Received on',
|
||||||
|
'mail_order_number' => 'Order number',
|
||||||
|
'mail_reference' => 'Reference number',
|
||||||
|
'mail_scope' => 'Scope of withdrawal',
|
||||||
|
'mail_items_hl' => 'Withdrawn items',
|
||||||
|
'mail_outro' => 'We will take care of the further processing and will contact you if necessary.',
|
||||||
|
'mail_greetings' => 'Best regards',
|
||||||
|
'mail_sender' => 'Your mivita.care team',
|
||||||
|
|
||||||
|
];
|
||||||
98
resources/views/admin/withdrawal/detail.blade.php
Normal file
98
resources/views/admin/withdrawal/detail.blade.php
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
@extends('layouts.layout-2')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h4 class="font-weight-bold py-2 mb-2">
|
||||||
|
<a href="{{ route('admin_withdrawals') }}" class="btn btn-sm btn-default float-right">{{ __('back') }}</a>
|
||||||
|
{{ __('navigation.withdrawals') }} <span class="text-muted">#{{ $withdrawal->id }}</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
@if (Session::has('alert-success'))
|
||||||
|
<div class="alert alert-success p-2 mt-2">{{ Session::get('alert-success') }}</div>
|
||||||
|
@endif
|
||||||
|
@if (Session::has('alert-error'))
|
||||||
|
<div class="alert alert-danger p-2 mt-2">{{ Session::get('alert-error') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-borderless mb-0">
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('withdrawal.success_received_at') }}</th>
|
||||||
|
<td>{{ optional($withdrawal->declared_at)->format('d.m.Y H:i') }} {{ __('withdrawal.clock') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('withdrawal.order_number') }}</th>
|
||||||
|
<td>
|
||||||
|
#{{ $withdrawal->order_reference }}
|
||||||
|
@if ($withdrawal->shopping_order_id)
|
||||||
|
<a href="{{ route('admin_sales_customers_detail', [$withdrawal->shopping_order->shopping_user_id ?? 0]) }}"
|
||||||
|
class="btn btn-xs btn-outline-primary ml-1"><i class="fa fa-external-link-alt"></i></a>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('withdrawal.choose_scope_hl') }}</th>
|
||||||
|
<td>
|
||||||
|
@if ($withdrawal->isFullWithdrawal())
|
||||||
|
{{ __('withdrawal.scope_full') }}
|
||||||
|
@else
|
||||||
|
{{ __('withdrawal.scope_partial') }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Kunde</th>
|
||||||
|
<td>{{ $withdrawal->customer_name }}<br><small>{{ $withdrawal->customer_email }}</small></td>
|
||||||
|
</tr>
|
||||||
|
@if ($withdrawal->user_shop)
|
||||||
|
<tr>
|
||||||
|
<th>Shop</th>
|
||||||
|
<td>{{ $withdrawal->user_shop->name }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@if ($withdrawal->reason)
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('withdrawal.reason') }}</th>
|
||||||
|
<td>{{ $withdrawal->reason }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (! empty($withdrawal->items))
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header font-weight-bold">{{ __('withdrawal.mail_items_hl') }}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($withdrawal->items as $item)
|
||||||
|
<li>{{ $item['qty'] }} × {{ $item['name'] }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header font-weight-bold">Status & Notiz</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{!! Form::open(['route' => ['admin_withdrawals_update', $withdrawal->id], 'method' => 'post']) !!}
|
||||||
|
<div class="form-group">
|
||||||
|
{!! Form::label('status', 'Status') !!}
|
||||||
|
{!! Form::select('status', collect($statuses)->mapWithKeys(fn ($s) => [$s => ucfirst($s)])->all(), $withdrawal->status, ['class' => 'form-control']) !!}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
{!! Form::label('admin_note', 'Interne Notiz') !!}
|
||||||
|
{!! Form::textarea('admin_note', $withdrawal->admin_note, ['class' => 'form-control', 'rows' => 5]) !!}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> {{ __('save') }}</button>
|
||||||
|
{!! Form::close() !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
57
resources/views/admin/withdrawal/index.blade.php
Normal file
57
resources/views/admin/withdrawal/index.blade.php
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
@extends('layouts.layout-2')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h4 class="font-weight-bold py-2 mb-2">
|
||||||
|
{{ __('navigation.withdrawals') }}
|
||||||
|
</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-datatable table-responsive">
|
||||||
|
<table class="datatables-withdrawals table table-striped table-bordered" id="datatables-withdrawals">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>{{ __('withdrawal.success_received_at') }}</th>
|
||||||
|
<th>{{ __('withdrawal.order_number') }}</th>
|
||||||
|
<th>Kunde</th>
|
||||||
|
<th>{{ __('withdrawal.choose_scope_hl') }}</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>#</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#datatables-withdrawals').DataTable({
|
||||||
|
"processing": true,
|
||||||
|
"serverSide": true,
|
||||||
|
ajax: {
|
||||||
|
url: '{!! route('admin_withdrawals_datatable') !!}',
|
||||||
|
data: function (d) {}
|
||||||
|
},
|
||||||
|
"order": [[0, "desc"]],
|
||||||
|
"columns": [
|
||||||
|
{data: 'id', name: 'id'},
|
||||||
|
{data: 'declared_at', name: 'declared_at'},
|
||||||
|
{data: 'order_reference', name: 'order_reference'},
|
||||||
|
{data: 'customer', name: 'customer'},
|
||||||
|
{data: 'type', name: 'type', searchable: false, orderable: false},
|
||||||
|
{data: 'status', name: 'status'},
|
||||||
|
{data: 'action', name: 'action', searchable: false, orderable: false},
|
||||||
|
],
|
||||||
|
"bLengthChange": false,
|
||||||
|
"iDisplayLength": 100,
|
||||||
|
"language": {
|
||||||
|
"url": "/js/datatables-{{ \App::getLocale() }}.json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
131
resources/views/emails/withdrawal_confirmation.blade.php
Normal file
131
resources/views/emails/withdrawal_confirmation.blade.php
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>mivita.care</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
width: 100%;
|
||||||
|
color: #37302d;
|
||||||
|
background: #f8f8f8;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
td, h1, h2, h3 {
|
||||||
|
font-family: Helvetica, Verdana, Arial, sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.headline {
|
||||||
|
color: #37302d;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.sub_headline {
|
||||||
|
color: #788662;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #b7c59e;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
background-color: #f6fdf5;
|
||||||
|
line-height: 1.6em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="padding:0; margin:0; background:#f8f8f8;" bgcolor="#f8f8f8">
|
||||||
|
<table align="left" cellpadding="0" cellspacing="0" width="100%" height="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left" valign="top" bgcolor="#f8f8f8" width="100%">
|
||||||
|
<br>
|
||||||
|
<table style="margin: 0 auto;" cellpadding="0" cellspacing="0" width="700">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<img src="https://my.mivita.care/images/logo_mivita.png" alt="mivita.care" style="border:none" width="230">
|
||||||
|
<br><br>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table cellpadding="10" style="padding: 10px;" cellspacing="0" border="0" width="100%" bgcolor="#ffffff">
|
||||||
|
<tr>
|
||||||
|
<td class="headline">
|
||||||
|
<b>{{ __('withdrawal.mail_hello') }} {{ $withdrawal->customer_name ?: '' }},</b>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="color:#37302d; line-height:1.6em;">
|
||||||
|
{{ __('withdrawal.mail_intro') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="info-box" cellpadding="2" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<b>{{ __('withdrawal.mail_received_at') }}:</b>
|
||||||
|
{{ optional($withdrawal->declared_at)->format('d.m.Y H:i') }} {{ __('withdrawal.clock') }}<br>
|
||||||
|
<b>{{ __('withdrawal.mail_order_number') }}:</b> #{{ $withdrawal->order_reference }}<br>
|
||||||
|
<b>{{ __('withdrawal.mail_reference') }}:</b> {{ $withdrawal->id }}<br>
|
||||||
|
<b>{{ __('withdrawal.mail_scope') }}:</b>
|
||||||
|
@if ($withdrawal->isFullWithdrawal())
|
||||||
|
{{ __('withdrawal.scope_full') }}
|
||||||
|
@else
|
||||||
|
{{ __('withdrawal.scope_partial') }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@if (! empty($withdrawal->items))
|
||||||
|
<tr>
|
||||||
|
<td class="sub_headline"><hr><b>{{ __('withdrawal.mail_items_hl') }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="info-box" cellpadding="2" cellspacing="0" width="100%">
|
||||||
|
@foreach ($withdrawal->items as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $item['qty'] }} × {{ $item['name'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@if ($withdrawal->reason)
|
||||||
|
<tr>
|
||||||
|
<td style="color:#37302d; line-height:1.6em;">
|
||||||
|
<hr>{{ $withdrawal->reason }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
<tr>
|
||||||
|
<td style="color:#37302d; line-height:1.6em;">
|
||||||
|
<hr>{{ __('withdrawal.mail_outro') }}
|
||||||
|
<br><br>
|
||||||
|
{{ __('withdrawal.mail_greetings') }}<br>
|
||||||
|
<b>{{ __('withdrawal.mail_sender') }}</b>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="color:#7B7B7E; font-size:12px; text-align: center; padding-top: 16px;" align="center">
|
||||||
|
mivita care gmbh • Leinfeld 2 • 87755 Kirchhaslach<br>
|
||||||
|
<a href="https://www.mivita.care" style="color: #7B7B7E;">www.mivita.care</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -315,7 +315,7 @@
|
||||||
<div>{{ __('navigation.clients') }}</div>
|
<div>{{ __('navigation.clients') }}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidenav-item @if (Request::is('admin/sales/*', 'admin/abos', 'admin/abos/*')) open @endif">
|
<li class="sidenav-item @if (Request::is('admin/sales/*', 'admin/abos', 'admin/abos/*', 'admin/withdrawals', 'admin/withdrawals/*')) open @endif">
|
||||||
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
<a href="javascript:void(0)" class="sidenav-link sidenav-toggle">
|
||||||
<i class="sidenav-icon ion ion-ios-cart"></i>
|
<i class="sidenav-icon ion ion-ios-cart"></i>
|
||||||
<div>{{ __('navigation.orders') }}</div>
|
<div>{{ __('navigation.orders') }}</div>
|
||||||
|
|
@ -341,6 +341,12 @@
|
||||||
<div>{{ __('navigation.abos') }}</div>
|
<div>{{ __('navigation.abos') }}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="sidenav-item{{ Request::is('admin/withdrawals', 'admin/withdrawals/*') ? ' active' : '' }}">
|
||||||
|
<a href="{{ route('admin_withdrawals') }}" class="sidenav-link"><i
|
||||||
|
class="sidenav-icon ion ion-md-undo"></i>
|
||||||
|
<div>{{ __('navigation.withdrawals') }}</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidenav-item{{ Request::is('admin/dhl/*') ? ' active' : '' }}">
|
<li class="sidenav-item{{ Request::is('admin/dhl/*') ? ' active' : '' }}">
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,16 @@
|
||||||
{{ __('shop.order_customer') }} <span class="text-muted">#{{$shopping_order->id}}</span>
|
{{ __('shop.order_customer') }} <span class="text-muted">#{{$shopping_order->id}}</span>
|
||||||
</h4>
|
</h4>
|
||||||
@include('portal.order._detail')
|
@include('portal.order._detail')
|
||||||
<a href="{{route('portal.my_orders')}}" class="btn btn-sm btn-default mt-2 float-right">{{ __('back') }}</a>
|
<div class="mt-2 d-flex justify-content-between align-items-center">
|
||||||
|
@if($shopping_order->payment_for === 6)
|
||||||
|
<a href="{{ route('withdrawal.create', ['order' => $shopping_order->id, 'email' => optional($shopping_order->shopping_user)->billing_email]) }}"
|
||||||
|
class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="fa fa-undo"></i> {{ __('withdrawal.button_label') }}
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span></span>
|
||||||
|
@endif
|
||||||
|
<a href="{{route('portal.my_orders')}}" class="btn btn-sm btn-default">{{ __('back') }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
@ -97,6 +97,9 @@
|
||||||
<li class=""><a target="_blank"
|
<li class=""><a target="_blank"
|
||||||
href="{{ asset('download/mivita_widerruf_formular.pdf') }}">{{ __('website.cancellation_policy') }} </a></li>
|
href="{{ asset('download/mivita_widerruf_formular.pdf') }}">{{ __('website.cancellation_policy') }} </a></li>
|
||||||
<li>•</li>
|
<li>•</li>
|
||||||
|
<li class="{{ Request::is('widerruf') ? ' active' : '' }}"><a
|
||||||
|
href="{{ route('withdrawal.create') }}">{{ __('withdrawal.button_label') }} </a></li>
|
||||||
|
<li>•</li>
|
||||||
<li class="{{ Request::is('datenschutz') ? ' active' : '' }}"><a
|
<li class="{{ Request::is('datenschutz') ? ' active' : '' }}"><a
|
||||||
href="{{ url('/datenschutz') }}">{{ __('website.privacy_policy') }} </a></li>
|
href="{{ url('/datenschutz') }}">{{ __('website.privacy_policy') }} </a></li>
|
||||||
<li>•</li>
|
<li>•</li>
|
||||||
|
|
|
||||||
49
resources/views/web/templates/widerruf-final.blade.php
Normal file
49
resources/views/web/templates/widerruf-final.blade.php
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
@extends($user_shop ? 'web.user.layouts.layout' : 'web.layouts.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<section class="page-header page-header-xlg parallax parallax-3"
|
||||||
|
style="background-image:url('/assets/images/vision-min.jpg')">
|
||||||
|
<div class="overlay dark-1"></div>
|
||||||
|
<div class="container"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel panel-default" style="max-width: 760px; margin: 0 auto;">
|
||||||
|
<div class="panel-body">
|
||||||
|
<h3><i class="fa fa-check-circle text-success"></i> {{ __('withdrawal.success_hl') }}</h3>
|
||||||
|
<p>{{ __('withdrawal.success_intro') }}</p>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{{ __('withdrawal.success_received_at') }}:</strong>
|
||||||
|
{{ optional($withdrawal->declared_at)->format('d.m.Y H:i') }} {{ __('withdrawal.clock') }}<br>
|
||||||
|
<strong>{{ __('withdrawal.order_number') }}:</strong> #{{ $withdrawal->order_reference }}<br>
|
||||||
|
<strong>{{ __('withdrawal.success_reference') }}:</strong> {{ $withdrawal->id }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
@if ($withdrawal->isFullWithdrawal())
|
||||||
|
{{ __('withdrawal.success_full') }}
|
||||||
|
@else
|
||||||
|
{{ __('withdrawal.success_partial') }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (! empty($withdrawal->items))
|
||||||
|
<ul>
|
||||||
|
@foreach ($withdrawal->items as $item)
|
||||||
|
<li>{{ $item['qty'] }} × {{ $item['name'] }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p>{{ __('withdrawal.success_email_sent', ['email' => $withdrawal->customer_email]) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@endsection
|
||||||
160
resources/views/web/templates/widerruf.blade.php
Normal file
160
resources/views/web/templates/widerruf.blade.php
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
@extends($user_shop ? 'web.user.layouts.layout' : 'web.layouts.layout')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<section class="page-header page-header-xlg parallax parallax-3"
|
||||||
|
style="background-image:url('/assets/images/vision-min.jpg')">
|
||||||
|
<div class="overlay dark-1"></div>
|
||||||
|
<div class="container"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.withdrawal-box {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.withdrawal-box .alert.error {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.withdrawal-scope label {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.withdrawal-items .qty-input {
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
.legal-note {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7758;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="withdrawal-box box-static box-transparent box-bordered padding-20">
|
||||||
|
<h1>{{ __('withdrawal.page_title') }}</h1>
|
||||||
|
<p>{{ __('withdrawal.intro') }}</p>
|
||||||
|
|
||||||
|
@if (session('alert-error'))
|
||||||
|
<div class="alert alert-danger error">{{ session('alert-error') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (! $order)
|
||||||
|
{{-- Phase 1: Bestellung identifizieren --}}
|
||||||
|
<hr>
|
||||||
|
<h3>{{ __('withdrawal.identify_hl') }}</h3>
|
||||||
|
{!! Form::open(['route' => 'withdrawal.lookup', 'method' => 'post']) !!}
|
||||||
|
<div class="form-group">
|
||||||
|
{!! Form::label('order_number', __('withdrawal.order_number') . '*') !!}
|
||||||
|
{!! Form::text('order_number', old('order_number', $prefill_order), ['class' => 'form-control ' . ($errors->has('order_number') ? 'error is-invalid' : ''), 'placeholder' => __('withdrawal.order_number_placeholder')]) !!}
|
||||||
|
<small class="form-text text-muted">{{ __('withdrawal.order_number_help') }}</small>
|
||||||
|
@if ($errors->has('order_number'))
|
||||||
|
<div class="alert alert-danger error">{{ $errors->first('order_number') }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
{!! Form::label('email', __('withdrawal.email') . '*') !!}
|
||||||
|
{!! Form::email('email', old('email', $prefill_email), ['class' => 'form-control ' . ($errors->has('email') ? 'error is-invalid' : ''), 'placeholder' => __('withdrawal.email_placeholder')]) !!}
|
||||||
|
@if ($errors->has('email'))
|
||||||
|
<div class="alert alert-danger error">{{ $errors->first('email') }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-text-500">
|
||||||
|
<i class="fa fa-search"></i> {{ __('withdrawal.find_order') }}
|
||||||
|
</button>
|
||||||
|
{!! Form::close() !!}
|
||||||
|
@else
|
||||||
|
{{-- Phase 2: Umfang wählen und absenden --}}
|
||||||
|
<hr>
|
||||||
|
<h3>{{ __('withdrawal.order_found_hl') }}</h3>
|
||||||
|
<p>
|
||||||
|
<strong>{{ __('withdrawal.order_number') }}:</strong> #{{ $order->id }}<br>
|
||||||
|
<strong>{{ __('withdrawal.order_date') }}:</strong> {{ optional($order->created_at)->format('d.m.Y H:i') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!! Form::open(['route' => 'withdrawal.store', 'method' => 'post']) !!}
|
||||||
|
{!! Form::hidden('order_number', $order->id) !!}
|
||||||
|
{!! Form::hidden('email', $prefill_email) !!}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h4>{{ __('withdrawal.choose_scope_hl') }}</h4>
|
||||||
|
<div class="withdrawal-scope">
|
||||||
|
<label class="radio">
|
||||||
|
{!! Form::radio('scope', \App\Models\OrderWithdrawal::TYPE_FULL, old('scope', 'full') === 'full', ['id' => 'scope_full']) !!}
|
||||||
|
<i></i> {{ __('withdrawal.scope_full') }}
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
{!! Form::radio('scope', \App\Models\OrderWithdrawal::TYPE_PARTIAL, old('scope') === 'partial', ['id' => 'scope_partial']) !!}
|
||||||
|
<i></i> {{ __('withdrawal.scope_partial') }}
|
||||||
|
</label>
|
||||||
|
@if ($errors->has('scope'))
|
||||||
|
<div class="alert alert-danger error">{{ $errors->first('scope') }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="withdrawal_items" class="withdrawal-items" style="display: none;">
|
||||||
|
<hr>
|
||||||
|
<h4>{{ __('withdrawal.select_items_hl') }}</h4>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('withdrawal.product') }}</th>
|
||||||
|
<th class="text-center">{{ __('withdrawal.quantity') }}</th>
|
||||||
|
<th class="text-center">{{ __('withdrawal.withdraw_quantity') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($order->shopping_order_items as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $item->product?->getLang('name') ?? ('#' . $item->product_id) }}</td>
|
||||||
|
<td class="text-center">{{ (int) $item->qty }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{!! Form::number('items[' . $item->id . ']', old('items.' . $item->id, 0), ['class' => 'form-control qty-input mx-auto', 'min' => 0, 'max' => (int) $item->qty]) !!}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="form-group">
|
||||||
|
{!! Form::label('reason', __('withdrawal.reason')) !!}
|
||||||
|
{!! Form::textarea('reason', old('reason'), ['class' => 'form-control', 'rows' => 3, 'placeholder' => __('withdrawal.reason_placeholder')]) !!}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="legal-note">{{ __('withdrawal.legal_note') }}</p>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{{ route('withdrawal.create') }}" class="btn btn-default btn-text-500">
|
||||||
|
<i class="fa fa-angle-left"></i> {{ __('withdrawal.back') }}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary btn-text-500">
|
||||||
|
<i class="fa fa-check"></i> {{ __('withdrawal.submit') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{!! Form::close() !!}
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
function toggleItems() {
|
||||||
|
if ($('#scope_partial').is(':checked')) {
|
||||||
|
$('#withdrawal_items').show();
|
||||||
|
} else {
|
||||||
|
$('#withdrawal_items').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('input[type=radio][name=scope]').on('change', toggleItems);
|
||||||
|
toggleItems();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -138,6 +138,9 @@
|
||||||
<li class=""><a target="_blank"
|
<li class=""><a target="_blank"
|
||||||
href="{{ asset('download/mivita_widerruf_formular.pdf') }}">{{ __('website.cancellation_policy') }}</a></li>
|
href="{{ asset('download/mivita_widerruf_formular.pdf') }}">{{ __('website.cancellation_policy') }}</a></li>
|
||||||
<li>•</li>
|
<li>•</li>
|
||||||
|
<li class="{{ Request::is('widerruf') ? ' active' : '' }}"><a
|
||||||
|
href="{{ route('withdrawal.create') }}">{{ __('withdrawal.button_label') }}</a></li>
|
||||||
|
<li>•</li>
|
||||||
<li class="{{ Request::is('datenschutz') ? ' active' : '' }}"><a
|
<li class="{{ Request::is('datenschutz') ? ' active' : '' }}"><a
|
||||||
href="{{ legal_url('/datenschutz') }}">{{ __('website.privacy_policy') }}</a></li>
|
href="{{ legal_url('/datenschutz') }}">{{ __('website.privacy_policy') }}</a></li>
|
||||||
<li>•</li>
|
<li>•</li>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Domain\EarlyDomainParser;
|
||||||
|
use App\Models\IqImage;
|
||||||
|
use App\Models\ProductImage;
|
||||||
|
use App\Models\UserShop;
|
||||||
|
use App\Models\UserShopOnSite;
|
||||||
|
use App\Services\LocaleGuard;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Geteilte Routen (Shared Routes)
|
| Geteilte Routen (Shared Routes)
|
||||||
|
|
@ -12,7 +19,7 @@
|
||||||
|
|
||||||
// Rechtliche und Kontakt-Routen - Umleitung zur Shop-Domain für checkout.*
|
// Rechtliche und Kontakt-Routen - Umleitung zur Shop-Domain für checkout.*
|
||||||
// Verwendet EarlyDomainParser für domain-awareness
|
// Verwendet EarlyDomainParser für domain-awareness
|
||||||
if (\App\Domain\EarlyDomainParser::getCurrentDomainType() === 'checkout') {
|
if (EarlyDomainParser::getCurrentDomainType() === 'checkout') {
|
||||||
// Shop-Domain aus Config laden
|
// Shop-Domain aus Config laden
|
||||||
$shopHost = config('domains.domains.shop.host');
|
$shopHost = config('domains.domains.shop.host');
|
||||||
$protocol = config('domains.protocol', 'https://');
|
$protocol = config('domains.protocol', 'https://');
|
||||||
|
|
@ -63,7 +70,7 @@ Route::post('/change_website_lang', 'Web\SiteController@changeLang')->name('lang
|
||||||
|
|
||||||
// Route für den Sprachwechsel (aus alter utility.php wiederhergestellt)
|
// Route für den Sprachwechsel (aus alter utility.php wiederhergestellt)
|
||||||
Route::get('translation/{locale}', function ($locale) {
|
Route::get('translation/{locale}', function ($locale) {
|
||||||
$normalized = \App\Services\LocaleGuard::normalize($locale);
|
$normalized = LocaleGuard::normalize($locale);
|
||||||
if ($normalized === null) {
|
if ($normalized === null) {
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +91,7 @@ Route::get('translation/{locale}', function ($locale) {
|
||||||
|
|
||||||
// Bild-Routen (aus alter utility.php wiederhergestellt)
|
// Bild-Routen (aus alter utility.php wiederhergestellt)
|
||||||
Route::get('/product/image/{slug}', function ($slug = null) {
|
Route::get('/product/image/{slug}', function ($slug = null) {
|
||||||
if ($image = \App\Models\ProductImage::where('slug', 'like', $slug)->first()) {
|
if ($image = ProductImage::where('slug', 'like', $slug)->first()) {
|
||||||
$path = storage_path('app/public').'/images/product'.'/'.$image->product_id.'/'.$image->filename;
|
$path = storage_path('app/public').'/images/product'.'/'.$image->product_id.'/'.$image->filename;
|
||||||
if (file_exists($path)) {
|
if (file_exists($path)) {
|
||||||
return response()->file($path);
|
return response()->file($path);
|
||||||
|
|
@ -95,7 +102,7 @@ Route::get('/product/image/{slug}', function ($slug = null) {
|
||||||
|
|
||||||
Route::get('storage/images/{from}/{slug}', function ($from = null, $slug = null) {
|
Route::get('storage/images/{from}/{slug}', function ($from = null, $slug = null) {
|
||||||
if ($from == 'shop') {
|
if ($from == 'shop') {
|
||||||
if ($image = \App\Models\UserShop::where('filename', $slug)->first()) {
|
if ($image = UserShop::where('filename', $slug)->first()) {
|
||||||
$path = storage_path('app/public').'/images/shop'.'/'.$image->filename;
|
$path = storage_path('app/public').'/images/shop'.'/'.$image->filename;
|
||||||
if (file_exists($path)) {
|
if (file_exists($path)) {
|
||||||
return Response::file($path);
|
return Response::file($path);
|
||||||
|
|
@ -105,7 +112,7 @@ Route::get('storage/images/{from}/{slug}', function ($from = null, $slug = null)
|
||||||
})->name('storage_images');
|
})->name('storage_images');
|
||||||
|
|
||||||
Route::get('/iq/image/{slug}', function ($slug = null) {
|
Route::get('/iq/image/{slug}', function ($slug = null) {
|
||||||
if ($image = \App\Models\IqImage::where('slug', $slug)->first()) {
|
if ($image = IqImage::where('slug', $slug)->first()) {
|
||||||
$path = storage_path('app/public').'/images/iq_images/'.$image->filename;
|
$path = storage_path('app/public').'/images/iq_images/'.$image->filename;
|
||||||
if (file_exists($path)) {
|
if (file_exists($path)) {
|
||||||
return Response::file($path);
|
return Response::file($path);
|
||||||
|
|
@ -114,7 +121,7 @@ Route::get('/iq/image/{slug}', function ($slug = null) {
|
||||||
})->name('iq_image');
|
})->name('iq_image');
|
||||||
|
|
||||||
Route::get('/user_shop/image/{slug}', function ($slug = null) {
|
Route::get('/user_shop/image/{slug}', function ($slug = null) {
|
||||||
if ($image = \App\Models\UserShopOnSite::where('slug', $slug)->first()) {
|
if ($image = UserShopOnSite::where('slug', $slug)->first()) {
|
||||||
$path = storage_path('app/public').'/images/user_shop'.'/'.$image->user_shop_id.'/'.$image->filename;
|
$path = storage_path('app/public').'/images/user_shop'.'/'.$image->user_shop_id.'/'.$image->filename;
|
||||||
if (file_exists($path)) {
|
if (file_exists($path)) {
|
||||||
return Response::file($path);
|
return Response::file($path);
|
||||||
|
|
@ -123,7 +130,7 @@ Route::get('/user_shop/image/{slug}', function ($slug = null) {
|
||||||
})->name('user_shop_image');
|
})->name('user_shop_image');
|
||||||
|
|
||||||
Route::get('/shop/product/image/{slug}', function ($slug = null) {
|
Route::get('/shop/product/image/{slug}', function ($slug = null) {
|
||||||
if ($image = \App\Models\ProductImage::where('slug', $slug)->first()) {
|
if ($image = ProductImage::where('slug', $slug)->first()) {
|
||||||
$path = storage_path('app/public').'/images/product'.'/'.$image->product_id.'/'.$image->filename;
|
$path = storage_path('app/public').'/images/product'.'/'.$image->product_id.'/'.$image->filename;
|
||||||
if (file_exists($path)) {
|
if (file_exists($path)) {
|
||||||
return Response::file($path);
|
return Response::file($path);
|
||||||
|
|
@ -134,7 +141,7 @@ Route::get('/shop/product/image/{slug}', function ($slug = null) {
|
||||||
// Alias für /home, damit auch /home erreichbar ist
|
// Alias für /home, damit auch /home erreichbar ist
|
||||||
|
|
||||||
// Checkout-Weiterleitung nur für Domains, die NICHT checkout.* sind
|
// Checkout-Weiterleitung nur für Domains, die NICHT checkout.* sind
|
||||||
if (\App\Domain\EarlyDomainParser::getCurrentDomainType() !== 'checkout') {
|
if (EarlyDomainParser::getCurrentDomainType() !== 'checkout') {
|
||||||
// Checkout-Domain aus Config laden
|
// Checkout-Domain aus Config laden
|
||||||
$checkoutHost = config('domains.domains.checkout.host');
|
$checkoutHost = config('domains.domains.checkout.host');
|
||||||
$protocol = config('domains.protocol', 'https://');
|
$protocol = config('domains.protocol', 'https://');
|
||||||
|
|
@ -169,3 +176,12 @@ if (\App\Domain\EarlyDomainParser::getCurrentDomainType() !== 'checkout') {
|
||||||
}
|
}
|
||||||
|
|
||||||
Route::get('/register/verify/{confirmationCode}', 'HomeController@verify')->name('register_verify');
|
Route::get('/register/verify/{confirmationCode}', 'HomeController@verify')->name('register_verify');
|
||||||
|
|
||||||
|
// Gesetzliche Online-Widerrufsfunktion (Widerrufs-Button-Pflicht ab 19.06.2026)
|
||||||
|
Route::get('/widerruf', 'Web\WithdrawalController@create')->name('withdrawal.create');
|
||||||
|
Route::post('/widerruf/pruefen', 'Web\WithdrawalController@lookup')
|
||||||
|
->middleware('throttle:20,1')
|
||||||
|
->name('withdrawal.lookup');
|
||||||
|
Route::post('/widerruf/absenden', 'Web\WithdrawalController@store')
|
||||||
|
->middleware('throttle:10,1')
|
||||||
|
->name('withdrawal.store');
|
||||||
|
|
|
||||||
192
tests/Feature/OrderWithdrawalTest.php
Normal file
192
tests/Feature/OrderWithdrawalTest.php
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Mail\MailWithdrawalConfirmation;
|
||||||
|
use App\Models\Country;
|
||||||
|
use App\Models\OrderWithdrawal;
|
||||||
|
use App\Models\ShoppingOrder;
|
||||||
|
use App\Models\ShoppingOrderItem;
|
||||||
|
use App\Models\ShoppingUser;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{order: ShoppingOrder, item: ShoppingOrderItem}
|
||||||
|
*/
|
||||||
|
function makeShopOrder(string $email = 'kunde@example.com'): array
|
||||||
|
{
|
||||||
|
makeShopEnv();
|
||||||
|
$country = Country::first();
|
||||||
|
$product = makeProduct(['1']);
|
||||||
|
|
||||||
|
$shoppingUser = ShoppingUser::create([
|
||||||
|
'auth_user_id' => 0,
|
||||||
|
'billing_firstname' => 'Max',
|
||||||
|
'billing_lastname' => 'Mustermann',
|
||||||
|
'billing_email' => $email,
|
||||||
|
'billing_country_id' => $country->id,
|
||||||
|
'shipping_country_id' => $country->id,
|
||||||
|
'is_from' => 'shopping',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$order = ShoppingOrder::create([
|
||||||
|
'shopping_user_id' => $shoppingUser->id,
|
||||||
|
'auth_user_id' => 0,
|
||||||
|
'user_shop_id' => 0,
|
||||||
|
'payment_for' => 6,
|
||||||
|
'country_id' => $country->id,
|
||||||
|
'language' => 'de',
|
||||||
|
'total' => 119,
|
||||||
|
'txaction' => 'paid',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = ShoppingOrderItem::create([
|
||||||
|
'shopping_order_id' => $order->id,
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'qty' => 2,
|
||||||
|
'price' => 59.5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['order' => $order, 'item' => $item];
|
||||||
|
}
|
||||||
|
|
||||||
|
it('zeigt die Widerrufsseite an', function () {
|
||||||
|
$this->get(route('withdrawal.create'))->assertOk();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zeigt nach erfolgreicher Identifikation die Bestellung mit Absende-Button', function () {
|
||||||
|
['order' => $order] = makeShopOrder('kunde@example.com');
|
||||||
|
|
||||||
|
$response = $this->post(route('withdrawal.lookup'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'kunde@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertSee('#'.$order->id)
|
||||||
|
->assertSee(__('withdrawal.submit'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('legt bei vollständigem Widerruf einen Datensatz an und versendet die Eingangsbestätigung', function () {
|
||||||
|
Mail::fake();
|
||||||
|
['order' => $order] = makeShopOrder('kunde@example.com');
|
||||||
|
|
||||||
|
$response = $this->post(route('withdrawal.store'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'kunde@example.com',
|
||||||
|
'scope' => 'full',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
$withdrawal = OrderWithdrawal::first();
|
||||||
|
expect($withdrawal)->not->toBeNull()
|
||||||
|
->and($withdrawal->shopping_order_id)->toBe($order->id)
|
||||||
|
->and($withdrawal->type)->toBe(OrderWithdrawal::TYPE_FULL)
|
||||||
|
->and($withdrawal->status)->toBe(OrderWithdrawal::STATUS_RECEIVED)
|
||||||
|
->and($withdrawal->customer_email)->toBe('kunde@example.com')
|
||||||
|
->and($withdrawal->declared_at)->not->toBeNull();
|
||||||
|
|
||||||
|
Mail::assertSent(MailWithdrawalConfirmation::class, function (MailWithdrawalConfirmation $mail) {
|
||||||
|
return $mail->hasTo('kunde@example.com')
|
||||||
|
&& $mail->hasBcc(config('app.withdrawal_mail'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('findet die Bestellung auch über Groß-/Kleinschreibung der E-Mail', function () {
|
||||||
|
Mail::fake();
|
||||||
|
['order' => $order] = makeShopOrder('kunde@example.com');
|
||||||
|
|
||||||
|
$this->post(route('withdrawal.store'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'KUNDE@EXAMPLE.COM',
|
||||||
|
'scope' => 'full',
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
expect(OrderWithdrawal::count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('speichert nur die ausgewählten Positionen beim teilweisen Widerruf', function () {
|
||||||
|
Mail::fake();
|
||||||
|
['order' => $order, 'item' => $item] = makeShopOrder();
|
||||||
|
|
||||||
|
$this->post(route('withdrawal.store'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'kunde@example.com',
|
||||||
|
'scope' => 'partial',
|
||||||
|
'items' => [$item->id => 1],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$withdrawal = OrderWithdrawal::first();
|
||||||
|
expect($withdrawal->type)->toBe(OrderWithdrawal::TYPE_PARTIAL)
|
||||||
|
->and($withdrawal->items)->toHaveCount(1)
|
||||||
|
->and($withdrawal->items[0]['qty'])->toBe(1)
|
||||||
|
->and($withdrawal->items[0]['shopping_order_item_id'])->toBe($item->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lehnt einen teilweisen Widerruf ohne ausgewählte Artikel ab', function () {
|
||||||
|
Mail::fake();
|
||||||
|
['order' => $order] = makeShopOrder();
|
||||||
|
|
||||||
|
$this->post(route('withdrawal.store'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'kunde@example.com',
|
||||||
|
'scope' => 'partial',
|
||||||
|
'items' => [],
|
||||||
|
])->assertRedirect();
|
||||||
|
|
||||||
|
expect(OrderWithdrawal::count())->toBe(0);
|
||||||
|
Mail::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('findet keine Bestellung bei falscher E-Mail', function () {
|
||||||
|
['order' => $order] = makeShopOrder('kunde@example.com');
|
||||||
|
|
||||||
|
$this->post(route('withdrawal.lookup'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'falsch@example.com',
|
||||||
|
])->assertRedirect();
|
||||||
|
|
||||||
|
expect(OrderWithdrawal::count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('widerruft keine Nicht-Shop-Bestellung (payment_for != 6)', function () {
|
||||||
|
makeShopEnv();
|
||||||
|
$country = Country::first();
|
||||||
|
$shoppingUser = ShoppingUser::create([
|
||||||
|
'auth_user_id' => 0,
|
||||||
|
'billing_firstname' => 'Max',
|
||||||
|
'billing_lastname' => 'Mustermann',
|
||||||
|
'billing_email' => 'kunde@example.com',
|
||||||
|
'billing_country_id' => $country->id,
|
||||||
|
'shipping_country_id' => $country->id,
|
||||||
|
'is_from' => 'user_order',
|
||||||
|
]);
|
||||||
|
$order = ShoppingOrder::create([
|
||||||
|
'shopping_user_id' => $shoppingUser->id,
|
||||||
|
'auth_user_id' => 0,
|
||||||
|
'user_shop_id' => 0,
|
||||||
|
'payment_for' => 3,
|
||||||
|
'country_id' => $country->id,
|
||||||
|
'language' => 'de',
|
||||||
|
'total' => 119,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->post(route('withdrawal.store'), [
|
||||||
|
'order_number' => (string) $order->id,
|
||||||
|
'email' => 'kunde@example.com',
|
||||||
|
'scope' => 'full',
|
||||||
|
])->assertRedirect();
|
||||||
|
|
||||||
|
expect(OrderWithdrawal::count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validiert Pflichtfelder', function () {
|
||||||
|
$this->post(route('withdrawal.store'), [
|
||||||
|
'order_number' => '',
|
||||||
|
'email' => 'keine-mail',
|
||||||
|
'scope' => 'full',
|
||||||
|
])->assertSessionHasErrors(['order_number', 'email']);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue