Abo: Fix Checkout-Rücksprung bei Einmalprodukten in der Bestellstrecke

Die Submit-Validierung checkSendYardForPayment() prüfte jede Cart-Zeile
gegen das Abo-Sortiment (12/13) – Einmal-Zeilen (show_on 3) fielen durch
und der Checkout-Klick sprang per back() kommentarlos auf die
Bestellseite zurück. Einmal-Zeilen werden jetzt gegen das
Einmal-Sortiment validiert (manipulierte Zeilen weiterhin abgewiesen).

Zusätzlich zeigt list.blade.php jetzt alle Validierungsfehler an
(vorher nur Comp-Produkt-Fehler → Fehler blieb unsichtbar).

Tests: 2 neue Regressionstests in AboCreateOneTimeCartTest (19 grün).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-22 13:02:43 +00:00
parent cdb121fde0
commit 657d4bd5b7
4 changed files with 93 additions and 1 deletions

View file

@ -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'));
}

View file

@ -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).

View file

@ -46,7 +46,7 @@
}
}
</style>
@if($errors->has('switchers-comp-product'))
@if($errors->any())
<div class="row">
<div class="col-sm-12">
<div class="alert alert-danger" id="gotocomp">

View file

@ -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);