From 9dc138e45b6a902b9c2f444abe3be6e0d8819b90 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 7 Jul 2026 08:17:27 +0000 Subject: [PATCH] Admin-Abo: Produkte trotz Mindestlaufzeit abziehen/entfernen Die Admin-Detailseite postete Warenkorb-Aktionen mit view=me/ot, wodurch Add-only-Modus und Change-Lock auch fuer Admins griffen (Reduzieren wurde verworfen, Entfernen lieferte 403). Formular postet jetzt mit view=admin; checkPermissions laesst view=admin nur noch fuer Admins zu (schliesst zugleich die Luecke, dass unbekannte Views alle Besitzer-Pruefungen uebersprangen). Co-authored-by: Cursor --- app/Http/Controllers/User/AboController.php | 12 +- resources/views/admin/abo/detail.blade.php | 2 +- .../AdminAboEditDuringMinDurationTest.php | 197 ++++++++++++++++++ 3 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/AdminAboEditDuringMinDurationTest.php diff --git a/app/Http/Controllers/User/AboController.php b/app/Http/Controllers/User/AboController.php index 4ee3b7bc..128cc872 100644 --- a/app/Http/Controllers/User/AboController.php +++ b/app/Http/Controllers/User/AboController.php @@ -145,8 +145,10 @@ class AboController extends Controller $this->checkPermissions($view, $user_abo); $editView = \Auth::user()?->isAdmin() ? 'admin' : $view; // 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. + // NICHT nach Admin-Status – auf der Selbstbedienungs-Seite (view me/ot) gelten für alle + // (auch VIP/Admin) die Berater-Regeln. Die Admin-Detailseite postet mit view=admin + // (nur für Admins zugelassen, siehe checkPermissions) und umgeht beide Sperren. + // $editView dient nur dem History-Logging. $isAddOnlyMode = AboHelper::isAddOnlyMode($user_abo, $view); $changeLocked = AboHelper::isAboChangeLocked($user_abo, $view); @@ -753,6 +755,12 @@ class AboController extends Controller return; } + // view=admin (und alle unbekannten Views) sind Nicht-Admins nicht erlaubt, + // sonst würden sämtliche is_for-/Besitzer-Prüfungen unten übersprungen. + if (! in_array($view, ['me', 'ot'], true)) { + abort(403, 'Unauthorized action. Invalid view'); + } + if ($view === 'me' && $user_abo->is_for !== 'me') { abort(403, 'Unauthorized action. Is not for me'); } diff --git a/resources/views/admin/abo/detail.blade.php b/resources/views/admin/abo/detail.blade.php index fb7e6981..96cbec87 100644 --- a/resources/views/admin/abo/detail.blade.php +++ b/resources/views/admin/abo/detail.blade.php @@ -45,7 +45,7 @@ {!! Form::open([ - 'action' => route('user_abos_update', [$view, $user_abo->id]), + 'action' => route('user_abos_update', ['admin', $user_abo->id]), 'class' => 'form-horizontal', 'id' => 'cart-order-form', 'data-add-only-mode' => '0', diff --git a/tests/Feature/AdminAboEditDuringMinDurationTest.php b/tests/Feature/AdminAboEditDuringMinDurationTest.php new file mode 100644 index 00000000..0d706773 --- /dev/null +++ b/tests/Feature/AdminAboEditDuringMinDurationTest.php @@ -0,0 +1,197 @@ + 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', + 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия', + ]); + $shipping = Shipping::create(['name' => 'Standard', 'active' => true]); + ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]); + + $account = UserAccount::create([ + 'salutation' => 'Herr', + 'first_name' => 'Max', + 'last_name' => 'Muster', + 'address' => 'Musterstr. 1', + 'zipcode' => '12345', + 'city' => 'Musterstadt', + 'country_id' => $country->id, + 'phone' => '123456', + 'same_as_billing' => 1, + 'language' => 'de', + ]); + + $consultant = User::forceCreate([ + 'email' => 'consultant-'.uniqid('', true).'@example.com', + 'password' => bcrypt('secret'), + 'lang' => 'de', + 'admin' => 0, + 'account_id' => $account->id, + ]); + + $shoppingUser = ShoppingUser::create([ + 'auth_user_id' => $consultant->id, + 'member_id' => $consultant->id, + 'billing_country_id' => $country->id, + 'shipping_country_id' => $country->id, + 'billing_email' => $consultant->email, + 'shipping_email' => $consultant->email, + 'billing_firstname' => 'Max', + 'billing_lastname' => 'Muster', + 'shipping_firstname' => 'Max', + 'shipping_lastname' => 'Muster', + 'same_as_billing' => 1, + 'is_for' => 'me', + 'is_from' => 'user_order', + ]); + + $abo = UserAbo::create([ + 'user_id' => $consultant->id, + 'member_id' => $consultant->id, + 'shopping_user_id' => $shoppingUser->id, + 'is_for' => 'me', + 'email' => $consultant->email, + 'payone_userid' => random_int(100000, 999999), + 'clearingtype' => 'cc', + 'active' => true, + 'status' => 2, + 'abo_interval' => 5, + 'next_date' => now()->addDays(30)->toDateString(), + ]); + + $productA = makeProduct(['12'], 50, 19); + $productB = makeProduct(['12'], 30, 19); + + $itemA = UserAboItem::create([ + 'user_abo_id' => $abo->id, 'product_id' => $productA->id, 'comp' => 0, 'qty' => 3, 'status' => 1, + ]); + $itemB = UserAboItem::create([ + 'user_abo_id' => $abo->id, 'product_id' => $productB->id, 'comp' => 0, 'qty' => 1, 'status' => 1, + ]); + + return ['abo' => $abo, 'consultant' => $consultant, 'itemA' => $itemA, 'itemB' => $itemB]; +} + +function makeAboAdminUser(): User +{ + return User::forceCreate([ + 'email' => 'admin-'.uniqid('', true).'@example.com', + 'password' => bcrypt('secret'), + 'lang' => 'de', + 'admin' => 2, + ]); +} + +/** + * Simuliert den AJAX-Call der Abo-Detailseite (iq-modal-cart.js) an AboController@update. + * + * @param array $payload + */ +function callAboCartUpdate(string $view, int $aboId, array $payload): mixed +{ + $request = app('request'); + $request->headers->set('X-Requested-With', 'XMLHttpRequest'); + $request->merge($payload); + + return (new AboController(app(AboRepository::class)))->update($view, $aboId); +} + +describe('Admin bearbeitet Abo-Produkte trotz Mindestlaufzeit', function () { + it('erlaubt dem Admin das Reduzieren der Menge während der Mindestlaufzeit', function () { + $fixture = makeMinDurationAboFixture(); + $this->actingAs(makeAboAdminUser()); + + $response = callAboCartUpdate('admin', $fixture['abo']->id, [ + 'action' => 'updateCart', + 'product_id' => $fixture['itemA']->product_id, + 'order_item_id' => $fixture['itemA']->id, + 'cart_order_id' => $fixture['abo']->id, + 'qty' => 2, + ]); + + expect($response->getStatusCode())->toBe(200) + ->and($fixture['itemA']->fresh()->qty)->toBe(2); + }); + + it('erlaubt dem Admin das Entfernen eines Produkts während der Mindestlaufzeit', function () { + $fixture = makeMinDurationAboFixture(); + $this->actingAs(makeAboAdminUser()); + + $response = callAboCartUpdate('admin', $fixture['abo']->id, [ + 'action' => 'removeFromCart', + 'product_id' => $fixture['itemB']->product_id, + 'order_item_id' => $fixture['itemB']->id, + 'cart_order_id' => $fixture['abo']->id, + ]); + + expect($response->getStatusCode())->toBe(200) + ->and(UserAboItem::find($fixture['itemB']->id))->toBeNull(); + }); + + it('blockiert das Reduzieren für den Berater weiterhin (Add-only, Menge bleibt)', function () { + $fixture = makeMinDurationAboFixture(); + $this->actingAs($fixture['consultant']); + + callAboCartUpdate('me', $fixture['abo']->id, [ + 'action' => 'updateCart', + 'product_id' => $fixture['itemA']->product_id, + 'order_item_id' => $fixture['itemA']->id, + 'cart_order_id' => $fixture['abo']->id, + 'qty' => 1, + ]); + + expect($fixture['itemA']->fresh()->qty)->toBe(3); + }); + + it('blockiert das Entfernen für den Berater weiterhin (Add-only, 403)', function () { + $fixture = makeMinDurationAboFixture(); + $this->actingAs($fixture['consultant']); + + $response = callAboCartUpdate('me', $fixture['abo']->id, [ + 'action' => 'removeFromCart', + 'product_id' => $fixture['itemB']->product_id, + 'order_item_id' => $fixture['itemB']->id, + 'cart_order_id' => $fixture['abo']->id, + ]); + + expect($response->getStatusCode())->toBe(403) + ->and(UserAboItem::find($fixture['itemB']->id))->not->toBeNull(); + }); + + it('verbietet Nicht-Admins die Admin-Sicht (view=admin => 403)', function () { + $fixture = makeMinDurationAboFixture(); + $this->actingAs($fixture['consultant']); + + expect(fn () => callAboCartUpdate('admin', $fixture['abo']->id, [ + 'action' => 'removeFromCart', + 'product_id' => $fixture['itemB']->product_id, + 'order_item_id' => $fixture['itemB']->id, + 'cart_order_id' => $fixture['abo']->id, + ]))->toThrow(HttpException::class); + }); +});