'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия', ]); $shipping = Shipping::create(['name' => 'Standard', 'active' => true]); ShippingPrice::create([ 'shipping_id' => $shipping->id, 'price' => 4.90, 'price_comp' => 0, 'num_comp' => 0, 'tax_rate' => 19, 'weight_from' => 1, 'weight_to' => 100000, ]); return ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]); } /** * Ruft OrderController::performRequest direkt auf (Domain-Routing hat einen Catch-All, * daher kein HTTP-Roundtrip). Liefert das dekodierte JSON-Payload zurück. * * @return array */ function postCartUpdate(array $payload): array { $request = request(); $request->replace(array_merge([ 'action' => 'updateCart', 'shipping_is_for' => 'abo-ot-customer', 'is_abo' => '1', ], $payload)); $request->headers->set('X-Requested-With', 'XMLHttpRequest'); $response = app(OrderController::class)->performRequest(); return $response->getData(true); } function findYardRows(int $productId, bool $addon): Collection { return Yard::instance('shopping')->search(function ($item) use ($productId, $addon) { return (int) $item->id === $productId && (bool) ($item->options->abo_addon ?? false) === $addon; }); } beforeEach(function () { $this->shippingCountry = makeOneTimeCartEnv(); $yard = Yard::instance('shopping'); $yard->destroy(); $yard->setShippingCountryWithPrice($this->shippingCountry->id, 'abo-ot-customer'); }); afterEach(function () { Yard::instance('shopping')->destroy(); }); it('liefert das Einmal-Sortiment (show_on 3) für Abo-Zusatzartikel', function () { expect(ProductOrderContext::oneTimeShowOnIds())->toBe(['3']); }); it('legt mit abo_addon=1 eine getrennte Einmal-Cart-Zeile an', function () { $product = makeProduct(['3'], 25.50, 19); $result = postCartUpdate(['product_id' => $product->id, 'qty' => 2, 'abo_addon' => 1]); expect($result['response'])->toBeTrue(); $rows = findYardRows($product->id, true); expect($rows)->toHaveCount(1) ->and((int) $rows->first()->qty)->toBe(2) ->and((bool) $rows->first()->options->abo_addon)->toBeTrue(); }); it('hält Abo-Zeile und Einmal-Zeile desselben Produkts getrennt', function () { $product = makeProduct(['3', '12'], 90, 19); postCartUpdate(['product_id' => $product->id, 'qty' => 1]); postCartUpdate(['product_id' => $product->id, 'qty' => 3, 'abo_addon' => 1]); $aboRows = findYardRows($product->id, false); $addonRows = findYardRows($product->id, true); expect($aboRows)->toHaveCount(1) ->and((int) $aboRows->first()->qty)->toBe(1) ->and($addonRows)->toHaveCount(1) ->and((int) $addonRows->first()->qty)->toBe(3) ->and($aboRows->first()->rowId)->not->toBe($addonRows->first()->rowId); }); it('lehnt ein Normalsortiment-Produkt ohne abo_addon-Flag im Abo-Cart weiter ab', function () { $product = makeProduct(['3'], 25.50, 19); $result = postCartUpdate(['product_id' => $product->id, 'qty' => 1]); expect($result['message'] ?? null)->toBe(__('msg.cart_product_not_allowed_for_order_type')) ->and(Yard::instance('shopping')->content())->toHaveCount(0); }); it('lehnt ein reines Abo-Produkt als Einmal-Artikel ab', function () { $product = makeProduct(['12'], 90, 19); $result = postCartUpdate(['product_id' => $product->id, 'qty' => 1, 'abo_addon' => 1]); expect($result['message'] ?? null)->toBe(__('msg.cart_product_not_allowed_for_order_type')) ->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); postCartUpdate(['product_id' => $product->id, 'qty' => 1]); postCartUpdate(['product_id' => $product->id, 'qty' => 2, 'abo_addon' => 1]); postCartUpdate(['product_id' => $product->id, 'qty' => 0, 'abo_addon' => 1]); expect(findYardRows($product->id, true))->toHaveCount(0) ->and(findYardRows($product->id, false))->toHaveCount(1); });