Abo Einmalprodukte: Phasen 5-7, konfigurierbare Sperrfristen, Lokalisierung & Dashboard
Rechnung/Lieferschein: - PDF-Gruppierung Abo- vs. Einmal-Artikel (is_abo_addon), rueckwaertskompatibel - Vorschau-Command sales:preview-invoice (umgeht Test-System-Gate, ohne Rechnungsnummer) Reminder-Mail (vorbereitet, Cron inaktiv): - MailAboExecutionReminder + Template, abo:send-execution-reminder, Spalte onetime_reminder_sent_for - Berater -> Sales Center, Kunde -> Portal; Vorschau abo:preview-reminder - Datum-Fix: formatDateDB() statt formatDate() (ICU-Format) Sperrfristen konfigurierbar (Admin-Settings): - abo-lock-days-change (Liefertag + Abo-Produkte), abo-lock-days-pause-cancel - Meldungen mit :lock-Platzhalter; Pausieren/Kuendigen vom Change-Lock entkoppelt - Abo-Produkt-Aenderungen im Lock-Fenster server- + clientseitig eingefroren - Selbstbedienung: Add-only/Lock nach $view (kein Admin-Bypass), Stale-Cache-Fix beim Mengen-Update Dashboard: - Einmalprodukte-Card + Team-Abo-KPI-Card, News-Card responsiv (col-xl-6) Lokalisierung: - Sprachdateien en/es/fr vollstaendig mit de synchronisiert (inkl. marketingplan/portal) - LocalizationSyncTest sichert Key-Gleichheit ab Tests: AboOneTimeInvoiceGrouping, AboExecutionReminder, AboLockDaysSetting, AboOnetimeDashboardAndMail, PreviewInvoiceCommand, TeamAboOverview + erweiterte View-Tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9f996a7f1b
commit
65b867f8ee
96 changed files with 4154 additions and 1189 deletions
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\User;
|
|||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Abo\AboOneTimeItemRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\ShoppingInstance;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboOneTimeItem;
|
||||
|
|
@ -13,6 +14,7 @@ use App\Services\AboHelper;
|
|||
use App\Services\AboItemHistoryService;
|
||||
use App\Services\AboOrderCart;
|
||||
use App\Services\AboRetryPaymentService;
|
||||
use App\Services\OrderPaymentService;
|
||||
use App\Services\Shop;
|
||||
use App\User;
|
||||
use Request;
|
||||
|
|
@ -33,15 +35,25 @@ class AboController extends Controller
|
|||
|
||||
if ($view === 'me') {
|
||||
// Nur Abos des aktuellen Benutzers
|
||||
$user_abos = UserAbo::where('user_id', \Auth::user()->id)
|
||||
->where('status', '>', 1);
|
||||
$baseQuery = UserAbo::where('user_id', \Auth::user()->id)
|
||||
->where('status', '>=', 1)
|
||||
->where('is_for', 'me');
|
||||
|
||||
if ((clone $baseQuery)->where('status', 1)->exists()) {
|
||||
return view('user.abo.index', [
|
||||
'user_abos' => (clone $baseQuery)->orderBy('id', 'desc')->get(),
|
||||
'view' => 'me',
|
||||
'isAdmin' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$user_abos = (clone $baseQuery)->orderBy('id', 'desc')->get();
|
||||
if ($user_abos->count() > 0) {
|
||||
return redirect(route('user_abos_detail', ['me', $user_abos->first()->id]));
|
||||
}
|
||||
|
||||
return view('user.abo.index', [
|
||||
'user_abos' => [],
|
||||
'user_abos' => $user_abos,
|
||||
'view' => 'me',
|
||||
'isAdmin' => false,
|
||||
]);
|
||||
|
|
@ -49,23 +61,28 @@ class AboController extends Controller
|
|||
|
||||
if ($view === 'ot') {
|
||||
$selectedYear = (int) \Request::get('year', now()->year);
|
||||
$selectedMonth = (int) \Request::get('month', 0);
|
||||
$baseQuery = UserAbo::where('member_id', \Auth::user()->id)
|
||||
->where('status', '>', 1)
|
||||
->where('status', '>=', 1)
|
||||
->where('is_for', 'ot');
|
||||
|
||||
$user_abos = (clone $baseQuery)
|
||||
->with(['user_abo_items', 'user_abo_items.product', 'shopping_user'])
|
||||
->addedInMonth($selectedYear, $selectedMonth)
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return view('user.abo.index', [
|
||||
'user_abos' => $user_abos,
|
||||
'abo_payment_links' => $this->getAboPaymentLinks(),
|
||||
'view' => 'ot',
|
||||
'isAdmin' => false,
|
||||
'chartData' => AboHelper::getMonthlyAboCounts($baseQuery, $selectedYear, 'ot', \Auth::user()->id),
|
||||
'chartYear' => $selectedYear,
|
||||
'chartYears' => \App\Services\HTMLHelper::getYearRange(2026),
|
||||
'chartMonths' => \App\Services\HTMLHelper::getTransMonths(),
|
||||
'filterMonth' => $selectedMonth,
|
||||
'filterYear' => $selectedYear,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +140,11 @@ class AboController extends Controller
|
|||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
$editView = \Auth::user()?->isAdmin() ? 'admin' : $view;
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $editView);
|
||||
// Add-only (Mindestlaufzeit) und Change-Lock richten sich nach der Abo-Sicht ($view),
|
||||
// NICHT nach Admin-Status – auf der Selbstbedienungs-Seite gelten für alle (auch VIP/Admin)
|
||||
// die Berater-Regeln. $editView dient nur dem History-Logging.
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $view);
|
||||
$changeLocked = AboHelper::isAboChangeLocked($user_abo, $view);
|
||||
|
||||
if (isset($data['action'])) {
|
||||
if ($data['action'] === 'abo_update_settings') {
|
||||
|
|
@ -138,8 +159,15 @@ class AboController extends Controller
|
|||
$message = false;
|
||||
// Yard für Gewichts-/Versandprüfung initialisieren (Versandland setzen)
|
||||
AboOrderCart::initYard($user_abo);
|
||||
|
||||
if ($changeLocked) {
|
||||
$message = __('abo.error_change_locked', [
|
||||
'days' => (int) now()->diffInDays(\Carbon\Carbon::parse($user_abo->next_date), false),
|
||||
'lock' => \App\Repositories\AboRepository::getLockDaysChange(),
|
||||
]);
|
||||
}
|
||||
// addProduct
|
||||
if ($data['action'] === 'addProduct') {
|
||||
if (! $changeLocked && $data['action'] === 'addProduct') {
|
||||
if ($product = Product::find($data['product_id'])) {
|
||||
if (AboOrderCart::exceedsMaxWeight($user_abo, (int) $product->weight)) {
|
||||
$message = __('msg.cart_max_weight_reached');
|
||||
|
|
@ -162,7 +190,7 @@ class AboController extends Controller
|
|||
}
|
||||
|
||||
// updateCart
|
||||
if ($data['action'] === 'updateCart') {
|
||||
if (! $changeLocked && $data['action'] === 'updateCart') {
|
||||
// product_id | order_item_id | cart_order_id | qty
|
||||
if (isset($data['product_id']) && $product = Product::find($data['product_id'])) {
|
||||
if (isset($data['order_item_id']) && $UserAboItem = UserAboItem::find($data['order_item_id'])) {
|
||||
|
|
@ -188,7 +216,7 @@ class AboController extends Controller
|
|||
}
|
||||
|
||||
// removeFromCart
|
||||
if ($data['action'] === 'removeFromCart') {
|
||||
if (! $changeLocked && $data['action'] === 'removeFromCart') {
|
||||
if ($isAddOnlyMode) {
|
||||
return response()->json([
|
||||
'response' => false,
|
||||
|
|
@ -212,7 +240,7 @@ class AboController extends Controller
|
|||
}
|
||||
}
|
||||
// updateCompProduct
|
||||
if ($data['action'] === 'updateCompProduct') {
|
||||
if (! $changeLocked && $data['action'] === 'updateCompProduct') {
|
||||
if ($UserAboItem = UserAboItem::where('user_abo_id', $user_abo->id)->where('comp', $data['comp_num'])->first()) {
|
||||
$oldProduct = $UserAboItem->product;
|
||||
$UserAboItem->product_id = $data['comp_product_id'];
|
||||
|
|
@ -239,6 +267,10 @@ class AboController extends Controller
|
|||
}
|
||||
AboOrderCart::checkNumOfCompProducts($user_abo); // after reCalculateShippingPrice check it and remove or add comp product
|
||||
|
||||
// Relation neu laden, damit die gerenderte Menge dem gespeicherten Stand
|
||||
// entspricht (sonst zeigt _order_abo_show die gecachte alte Menge → Feld „springt" zurück).
|
||||
$user_abo->load('user_abo_items');
|
||||
|
||||
if ($user_abo->is_for === 'me') {
|
||||
$data['comp_products'] = Shop::getCompProducts('abo-me');
|
||||
}
|
||||
|
|
@ -250,6 +282,7 @@ class AboController extends Controller
|
|||
'split_mode' => AboHelper::isOneTimeFeatureVisible($user_abo),
|
||||
'summary' => $summary,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
'change_locked' => $changeLocked,
|
||||
])->render();
|
||||
$html_comp = view('user.order.comp_product', array_merge($data, [
|
||||
'cart_instance' => AboOrderCart::INSTANCE,
|
||||
|
|
@ -271,8 +304,9 @@ class AboController extends Controller
|
|||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
$editView = \Auth::user()?->isAdmin() ? 'admin' : $view;
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $editView);
|
||||
// Add-only/Change-Lock nach Abo-Sicht ($view), kein Admin-Bypass (siehe update()).
|
||||
$isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $view);
|
||||
$changeLocked = AboHelper::isAboChangeLocked($user_abo, $view);
|
||||
|
||||
if (! AboHelper::isOneTimeFeatureVisible($user_abo)) {
|
||||
return response()->json([
|
||||
|
|
@ -310,6 +344,7 @@ class AboController extends Controller
|
|||
'summary' => $summary,
|
||||
'only_show_products' => false,
|
||||
'add_only_mode' => $isAddOnlyMode,
|
||||
'change_locked' => $changeLocked,
|
||||
])->render(),
|
||||
'html_summary' => view('admin.abo._order_combined_summary', [
|
||||
'summary' => $summary,
|
||||
|
|
@ -520,6 +555,67 @@ class AboController extends Controller
|
|||
return redirect(route('user_abos_detail', [$view, $id]));
|
||||
}
|
||||
|
||||
public function deletePending($view, $id)
|
||||
{
|
||||
$user_abo = UserAbo::findOrFail($id);
|
||||
$this->checkPermissions($view, $user_abo);
|
||||
|
||||
if (! $user_abo->canDeletePendingInitialPayment()) {
|
||||
\Session()->flash('alert-error', __('abo.pending_delete_not_allowed'));
|
||||
|
||||
return redirect(route('user_abos', [$view]));
|
||||
}
|
||||
|
||||
\DB::transaction(function () use ($user_abo) {
|
||||
$user_abo->delete();
|
||||
});
|
||||
|
||||
\Session()->flash('alert-success', __('abo.pending_delete_success'));
|
||||
|
||||
return redirect(route('user_abos', [$view]));
|
||||
}
|
||||
|
||||
public function deletePaymentLink($identifier)
|
||||
{
|
||||
$shoppingInstance = ShoppingInstance::where('identifier', $identifier)->firstOrFail();
|
||||
$user = \Auth::user();
|
||||
$userShopId = $user && $user->shop ? $user->shop->id : null;
|
||||
|
||||
if ((int) $shoppingInstance->user_shop_id !== (int) $userShopId || ! OrderPaymentService::isAboCustomerPaymentLink($shoppingInstance)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (! OrderPaymentService::canDeleteInstance($shoppingInstance)) {
|
||||
\Session()->flash('alert-error', __('abo.payment_link_delete_not_allowed'));
|
||||
|
||||
return redirect(route('user_abos', ['ot']));
|
||||
}
|
||||
|
||||
OrderPaymentService::deleteInstance($identifier);
|
||||
\Session()->flash('alert-success', __('abo.payment_link_delete_success'));
|
||||
|
||||
return redirect(route('user_abos', ['ot']));
|
||||
}
|
||||
|
||||
private function getAboPaymentLinks()
|
||||
{
|
||||
$user = \Auth::user();
|
||||
$userShopId = $user && $user->shop ? $user->shop->id : null;
|
||||
|
||||
if (! $userShopId) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ShoppingInstance::where('user_shop_id', $userShopId)
|
||||
->where('payment', 6)
|
||||
->where('status', '<', 10)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->filter(fn (ShoppingInstance $shoppingInstance) => OrderPaymentService::isAboCustomerPaymentLink($shoppingInstance))
|
||||
->reject(fn (ShoppingInstance $shoppingInstance) => OrderPaymentService::hasUserAboForPaymentLink($shoppingInstance, $user))
|
||||
->values();
|
||||
}
|
||||
|
||||
private function checkPermissions($view, $user_abo)
|
||||
{
|
||||
\Log::info('checkPermissions', ['view' => $view, 'user_abo' => $user_abo]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue