diff --git a/app/Http/Controllers/User/OrderController.php b/app/Http/Controllers/User/OrderController.php index 5ee3b7d5..724f77f9 100644 --- a/app/Http/Controllers/User/OrderController.php +++ b/app/Http/Controllers/User/OrderController.php @@ -476,6 +476,14 @@ class OrderController extends Controller if (! $product) { continue; } + if ($isAbo && ($row->options->abo_addon ?? false)) { + // Einmal-Zeilen (nur erste Lieferung) gehören zum Einmal-Sortiment, nicht zum Abo-Sortiment + if (! ProductOrderContext::productMatchesShowOn($product, ProductOrderContext::oneTimeShowOnIds())) { + throw new \Exception(__('msg.cart_product_not_allowed_for_order_type')); + } + + continue; + } if (! ProductOrderContext::isProductAllowedInContext($product, $isAbo, $data['shipping_is_for'])) { throw new \Exception(__('msg.cart_product_not_allowed_for_order_type')); } diff --git a/dev/2026-07-21-abo-einmalprodukte-anlage/ENTWICKLUNGSPLAN-EINMALPRODUKTE-BEI-ABO-ANLAGE.md b/dev/2026-07-21-abo-einmalprodukte-anlage/ENTWICKLUNGSPLAN-EINMALPRODUKTE-BEI-ABO-ANLAGE.md index 334887e7..1dd1b225 100644 --- a/dev/2026-07-21-abo-einmalprodukte-anlage/ENTWICKLUNGSPLAN-EINMALPRODUKTE-BEI-ABO-ANLAGE.md +++ b/dev/2026-07-21-abo-einmalprodukte-anlage/ENTWICKLUNGSPLAN-EINMALPRODUKTE-BEI-ABO-ANLAGE.md @@ -201,6 +201,23 @@ Kein neues Datenmodell, keine Migration, kein Purge nötig. --- +## 8a. Nachtrag — Bugfix Checkout-Rücksprung (2026-07-22) + +**Status:** ERLEDIGT + +**Gemeldet:** Auf `/user/order/my/list/abo-me` sprang der Klick auf „Checkout" kommentarlos an den Seitenanfang zurück, sobald ein Einmalprodukt im Warenkorb lag. + +**Ursache:** `OrderController::checkSendYardForPayment()` (Z. ~473) re-validiert beim Absenden JEDE Cart-Zeile gegen das Abo-Sortiment (`isProductAllowedInContext` → `['12','13']`). Einmal-Zeilen (`show_on '3'`) fielen durch → Exception → `back()->withErrors(['error' => …])`. Zweites Problem: `list.blade.php` renderte nur `switchers-comp-product`-Fehler — der eigentliche Fehler blieb unsichtbar (daher „kommentarloser" Rücksprung). + +**Fix:** +- `checkSendYardForPayment()`: Einmal-Zeilen (`abo_addon`) werden gegen `ProductOrderContext::oneTimeShowOnIds()` validiert statt gegen das Abo-Sortiment (manipulierte Addon-Zeilen mit reinen Abo-Produkten werden weiterhin abgewiesen). +- `list.blade.php`: Fehlerblock zeigt jetzt `$errors->any()` statt nur Comp-Produkt-Fehler — künftige Validierungsfehler sind sichtbar. +- Tests: 2 neue in `AboCreateOneTimeCartTest` (Passieren mit Addon-Zeile, Abweisen manipulierter Addon-Zeile), gesamt 19 Feature-Tests grün. + +**Lesson learned:** Die Checkout-Submit-Validierung war ein zweites, beim initialen Grep übersehenes Sortiment-Gate (neben `handleUpdateCart`); alle `isProductAllowedInContext`-Aufrufer wurden jetzt vollständig geprüft (keine weiteren Gates). + +--- + ## 9. Umsetzungs-Zusammenfassung (2026-07-21) Alle 7 Phasen umgesetzt. Kern-Mechanik: Cart-Option `abo_addon` → `shopping_order_items.is_abo_addon` (Checkout persistiert jetzt) → PDF-Gruppierung automatisch; `createAboItems`/`ensureUserAboItemsFromLatestOrder` überspringen Addons; `user_abos.amount` ohne Addon-Anteil. Kein neues Datenmodell, keine Migration. Neue Tests: `AboCreateOneTimeItemsTest` (6), `AboCreateOneTimeCartTest` (6), `AboPortalCreateOneTimeTest` (5). diff --git a/resources/views/user/order/list.blade.php b/resources/views/user/order/list.blade.php index cb8953b2..fdeed1dd 100644 --- a/resources/views/user/order/list.blade.php +++ b/resources/views/user/order/list.blade.php @@ -46,7 +46,7 @@ } } - @if($errors->has('switchers-comp-product')) + @if($errors->any())
diff --git a/tests/Feature/AboCreateOneTimeCartTest.php b/tests/Feature/AboCreateOneTimeCartTest.php index 2fd372ce..f61e88d7 100644 --- a/tests/Feature/AboCreateOneTimeCartTest.php +++ b/tests/Feature/AboCreateOneTimeCartTest.php @@ -11,7 +11,9 @@ use App\Models\Country; use App\Models\Shipping; use App\Models\ShippingCountry; use App\Models\ShippingPrice; +use App\Models\UserAccount; use App\Services\ProductOrderContext; +use App\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Collection; use Tests\TestCase; @@ -122,6 +124,71 @@ it('lehnt ein reines Abo-Produkt als Einmal-Artikel ab', function () { ->and(Yard::instance('shopping')->content())->toHaveCount(0); }); +/** + * Eingeloggter Berater mit Versandland-Account (Voraussetzung für checkSendYardForPayment). + */ +function makeCheckoutValidationUser(ShippingCountry $shippingCountry): User +{ + $account = UserAccount::create([ + 'salutation' => 'Herr', + 'first_name' => 'Max', + 'last_name' => 'Muster', + 'address' => 'Musterstr. 1', + 'zipcode' => '12345', + 'city' => 'Musterstadt', + 'country_id' => $shippingCountry->country_id, + 'phone' => '123456', + 'same_as_billing' => 1, + 'language' => 'de', + ]); + + $user = User::forceCreate([ + 'email' => 'checkout-'.uniqid('', true).'@example.com', + 'password' => bcrypt('secret'), + 'lang' => 'de', + 'account_id' => $account->id, + ]); + + test()->be($user); + + return $user; +} + +function invokeCheckSendYardForPayment(): void +{ + $controller = app(OrderController::class); + $reflection = new ReflectionMethod($controller, 'checkSendYardForPayment'); + $reflection->invoke($controller, ['shipping_is_for' => 'abo-me'], null); +} + +it('lässt die Checkout-Validierung Einmal-Zeilen im Abo-Cart passieren', function () { + makeCheckoutValidationUser($this->shippingCountry); + $aboProduct = makeProduct(['12'], 90, 19); + $addonProduct = makeProduct(['3'], 25.50, 19); + + postCartUpdate(['product_id' => $aboProduct->id, 'qty' => 1, 'shipping_is_for' => 'abo-me']); + postCartUpdate(['product_id' => $addonProduct->id, 'qty' => 2, 'abo_addon' => 1, 'shipping_is_for' => 'abo-me']); + + // Vor dem Fix warf die Sortiment-Prüfung hier "cart_product_not_allowed_for_order_type" + // und der Checkout-Klick sprang kommentarlos auf die Bestellseite zurück. + invokeCheckSendYardForPayment(); + + expect(findYardRows($addonProduct->id, true))->toHaveCount(1); +}); + +it('weist manipulierte Einmal-Zeilen mit Abo-Produkt bei der Checkout-Validierung ab', function () { + makeCheckoutValidationUser($this->shippingCountry); + $aboProduct = makeProduct(['12'], 90, 19); + + postCartUpdate(['product_id' => $aboProduct->id, 'qty' => 1, 'shipping_is_for' => 'abo-me']); + Yard::instance('shopping')->add($aboProduct->id, 'Manipuliert', 1, 90.0, false, false, [ + 'slug' => $aboProduct->slug, 'weight' => 500, 'points' => 10, 'show_on' => ['12'], + 'abo_addon' => true, + ]); + + invokeCheckSendYardForPayment(); +})->throws(Exception::class); + it('entfernt mit qty=0 nur die Einmal-Zeile, die Abo-Zeile bleibt', function () { $product = makeProduct(['3', '12'], 90, 19);