Abo: Einmalprodukte bereits bei der Abo-Anlage + es/fr-Sprachsync
Einmalprodukte (Normalsortiment, show_on 3) können jetzt schon beim Abo-Abschluss hinzugefügt werden – im Portal-Wizard (Abschnitt im Upgrade-Schritt) und in allen drei Sales-Center-Abo-Strecken (eigene Produktliste). Variante A: Zeilen laufen als Cart-Option abo_addon mit durch den Checkout, werden sofort bezahlt und mit der Erstlieferung verschickt (Rechnung/Lieferschein gruppieren automatisch). - Checkout persistiert is_abo_addon (CheckoutRepository, PaymentHelper) - createAboItems/ensureUserAboItemsFromLatestOrder überspringen Addons (sonst würden Einmalprodukte dauerhaft wiederkehren); amount ohne Addon-Anteil; aboHasBaseProduct ignoriert Addon-Zeilen - CreateAboReplacementOrder propagiert is_abo_addon in Ersatzbestellungen - iq-shopping-cart.js: zweite Datatable + abo_addon-Flag-Weitergabe - i18n: neue abo.onetime_create_*-Keys (de/en/es/fr); zusätzlich es/fr komplett synchronisiert (abo-Kartenblock, navigation, payment, weborder inkl. Entfernung PAYONE-Altlasten, withdrawal.php neu) - Tests: AboCreateOneTimeItemsTest, AboCreateOneTimeCartTest, AboPortalCreateOneTimeTest (17 neue, 122 Tests gesamt grün) Plan: dev/2026-07-21-abo-einmalprodukte-anlage/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1e7ae92d0c
commit
cdb121fde0
32 changed files with 1479 additions and 52 deletions
134
tests/Feature/AboCreateOneTimeCartTest.php
Normal file
134
tests/Feature/AboCreateOneTimeCartTest.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Einmalprodukte in der Sales-Center-Abo-Bestellstrecke (Anlage, Variante A):
|
||||
* updateCart mit abo_addon=1 legt eine getrennte Cart-Zeile (Option abo_addon) an,
|
||||
* Sortiment-Prüfung läuft gegen das Einmal-Sortiment (show_on '3').
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\User\OrderController;
|
||||
use App\Models\Country;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShippingPrice;
|
||||
use App\Services\ProductOrderContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function makeOneTimeCartEnv(): ShippingCountry
|
||||
{
|
||||
$country = Country::create([
|
||||
'code' => '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<string, mixed>
|
||||
*/
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
274
tests/Feature/AboCreateOneTimeItemsTest.php
Normal file
274
tests/Feature/AboCreateOneTimeItemsTest.php
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Einmalprodukte bei der Abo-Anlage (Variante A, Plan dev/2026-07-21-abo-einmalprodukte-anlage):
|
||||
* Positionen mit Cart-Option `abo_addon` werden beim Checkout als `is_abo_addon` persistiert,
|
||||
* fließen NICHT in die dauerhaften Abo-Positionen (user_abo_items) und nicht in user_abos.amount.
|
||||
*/
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShippingPrice;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAboItem;
|
||||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserShop;
|
||||
use App\Services\AboHelper;
|
||||
use App\Services\PaymentHelper;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* @return array{user: User, shoppingUser: ShoppingUser, shippingCountry: ShippingCountry, userShop: UserShop}
|
||||
*/
|
||||
function aboCreateOneTimeFixture(): array
|
||||
{
|
||||
$country = Country::create([
|
||||
'code' => '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,
|
||||
]);
|
||||
$shippingCountry = ShippingCountry::create(['shipping_id' => $shipping->id, 'country_id' => $country->id]);
|
||||
|
||||
$consultant = User::forceCreate([
|
||||
'email' => 'consultant-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
|
||||
$userShop = UserShop::create([
|
||||
'user_id' => $consultant->id,
|
||||
'name' => 'TS'.substr(uniqid('', true), 0, 8),
|
||||
'slug' => 'ts-'.uniqid(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$shoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $consultant->id,
|
||||
'member_id' => $consultant->id,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'billing_email' => 'cust-'.uniqid('', true).'@example.com',
|
||||
'is_for' => 'ot',
|
||||
'is_from' => 'shopping',
|
||||
]);
|
||||
|
||||
return [
|
||||
'user' => $consultant,
|
||||
'shoppingUser' => $shoppingUser,
|
||||
'shippingCountry' => $shippingCountry,
|
||||
'userShop' => $userShop,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abo-Erstbestellung mit einem regulären Abo-Artikel (90,00 €) und einem
|
||||
* Einmal-Artikel (25,50 € x 2, is_abo_addon) plus bezahlter ShoppingPayment (100,00 €).
|
||||
*
|
||||
* @return array{order: ShoppingOrder, payment: ShoppingPayment, aboProductId: int, addonProductId: int}
|
||||
*/
|
||||
function aboCreateOrderWithAddon(array $fixture): array
|
||||
{
|
||||
$aboProduct = makeProduct(['12'], 90, 19);
|
||||
$addonProduct = makeProduct(['3'], 25.50, 19);
|
||||
|
||||
$order = ShoppingOrder::create([
|
||||
'shopping_user_id' => $fixture['shoppingUser']->id,
|
||||
'auth_user_id' => $fixture['user']->id,
|
||||
'member_id' => $fixture['user']->id,
|
||||
'country_id' => $fixture['shippingCountry']->id,
|
||||
'user_shop_id' => $fixture['userShop']->id,
|
||||
'payment_for' => 6,
|
||||
'points' => 10,
|
||||
'is_abo' => true,
|
||||
'abo_interval' => 15,
|
||||
'paid' => false,
|
||||
'txaction' => 'prev',
|
||||
'mode' => 'test',
|
||||
'total' => 141,
|
||||
'subtotal' => 118.49,
|
||||
'total_shipping' => 145.90,
|
||||
]);
|
||||
|
||||
ShoppingOrderItem::create([
|
||||
'shopping_order_id' => $order->id,
|
||||
'row_id' => 'row-abo',
|
||||
'product_id' => $aboProduct->id,
|
||||
'comp' => 0,
|
||||
'qty' => 1,
|
||||
'price' => 90,
|
||||
'price_net' => 75.63,
|
||||
'tax_rate' => 19,
|
||||
'tax' => 14.37,
|
||||
'slug' => $aboProduct->slug,
|
||||
'is_abo_addon' => false,
|
||||
]);
|
||||
ShoppingOrderItem::create([
|
||||
'shopping_order_id' => $order->id,
|
||||
'row_id' => 'row-addon',
|
||||
'product_id' => $addonProduct->id,
|
||||
'comp' => 0,
|
||||
'qty' => 2,
|
||||
'price' => 25.50,
|
||||
'price_net' => 21.43,
|
||||
'tax_rate' => 19,
|
||||
'tax' => 4.07,
|
||||
'slug' => $addonProduct->slug,
|
||||
'is_abo_addon' => true,
|
||||
]);
|
||||
|
||||
$payment = ShoppingPayment::create([
|
||||
'shopping_order_id' => $order->id,
|
||||
'reference' => 'ref-'.uniqid(),
|
||||
'amount' => 10000,
|
||||
'currency' => 'EUR',
|
||||
'clearingtype' => 'cc',
|
||||
'abo_interval' => 15,
|
||||
'mode' => 'test',
|
||||
]);
|
||||
|
||||
PaymentTransaction::create([
|
||||
'shopping_payment_id' => $payment->id,
|
||||
'request' => 'transaction',
|
||||
'txid' => 123456,
|
||||
'userid' => 987654,
|
||||
'status' => 'PAYONE',
|
||||
'txaction' => 'paid',
|
||||
'mode' => 'test',
|
||||
]);
|
||||
|
||||
return [
|
||||
'order' => $order,
|
||||
'payment' => $payment,
|
||||
'aboProductId' => $aboProduct->id,
|
||||
'addonProductId' => $addonProduct->id,
|
||||
];
|
||||
}
|
||||
|
||||
it('persistiert die Cart-Option abo_addon als is_abo_addon auf der Checkout-Bestellung', function () {
|
||||
$fixture = aboCreateOneTimeFixture();
|
||||
|
||||
$aboProduct = makeProduct(['12'], 90, 19);
|
||||
$addonProduct = makeProduct(['3'], 25.50, 19);
|
||||
|
||||
$yard = Yard::instance('shopping');
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($fixture['shippingCountry']->id);
|
||||
|
||||
$yard->add($aboProduct->id, 'Abo-Produkt', 1, 90.0, false, false, [
|
||||
'slug' => $aboProduct->slug, 'weight' => 500, 'points' => 10,
|
||||
'no_commission' => false, 'no_free_shipping' => false, 'show_on' => ['12'],
|
||||
]);
|
||||
$yard->add($addonProduct->id, 'Einmal-Produkt', 2, 25.50, false, false, [
|
||||
'slug' => $addonProduct->slug, 'weight' => 200, 'points' => 4,
|
||||
'no_commission' => false, 'no_free_shipping' => false, 'show_on' => ['3'],
|
||||
'abo_addon' => true,
|
||||
]);
|
||||
|
||||
$order = (new PaymentHelper)->makeShoppingOrder($fixture['user'], $fixture['shoppingUser']);
|
||||
|
||||
$aboItem = $order->shopping_order_items()->where('product_id', $aboProduct->id)->first();
|
||||
$addonItem = $order->shopping_order_items()->where('product_id', $addonProduct->id)->first();
|
||||
|
||||
expect($aboItem)->not->toBeNull()
|
||||
->and((bool) $aboItem->is_abo_addon)->toBeFalse()
|
||||
->and($addonItem)->not->toBeNull()
|
||||
->and((bool) $addonItem->is_abo_addon)->toBeTrue()
|
||||
->and((int) $addonItem->qty)->toBe(2);
|
||||
|
||||
$yard->destroy();
|
||||
});
|
||||
|
||||
it('übernimmt Einmal-Artikel bei der Abo-Anlage NICHT in die dauerhaften Abo-Positionen', function () {
|
||||
$fixture = aboCreateOneTimeFixture();
|
||||
$orderData = aboCreateOrderWithAddon($fixture);
|
||||
|
||||
AboHelper::createNewAbo($orderData['payment']);
|
||||
|
||||
$userAbo = UserAbo::first();
|
||||
expect($userAbo)->not->toBeNull();
|
||||
|
||||
$items = UserAboItem::where('user_abo_id', $userAbo->id)->get();
|
||||
expect($items)->toHaveCount(1)
|
||||
->and((int) $items->first()->product_id)->toBe($orderData['aboProductId']);
|
||||
});
|
||||
|
||||
it('setzt user_abos.amount bei der Anlage ohne den Einmal-Artikel-Anteil', function () {
|
||||
$fixture = aboCreateOneTimeFixture();
|
||||
$orderData = aboCreateOrderWithAddon($fixture);
|
||||
|
||||
AboHelper::createNewAbo($orderData['payment']);
|
||||
|
||||
// Zahlung 100,00 € (10000 Cent) minus Addon-Brutto 2 x 25,50 € = 51,00 € -> 4900 Cent
|
||||
expect((int) UserAbo::first()->amount)->toBe(4900);
|
||||
});
|
||||
|
||||
it('lässt amount unverändert wenn die Erstbestellung keine Einmal-Artikel enthält', function () {
|
||||
$fixture = aboCreateOneTimeFixture();
|
||||
$orderData = aboCreateOrderWithAddon($fixture);
|
||||
ShoppingOrderItem::where('shopping_order_id', $orderData['order']->id)
|
||||
->where('is_abo_addon', true)
|
||||
->delete();
|
||||
|
||||
AboHelper::createNewAbo($orderData['payment']);
|
||||
|
||||
expect((int) UserAbo::first()->amount)->toBe(10000);
|
||||
});
|
||||
|
||||
it('gruppiert Erstrechnung und Lieferschein der Anlage-Bestellung nach Einmal-Artikeln', function () {
|
||||
$fixture = aboCreateOneTimeFixture();
|
||||
$orderData = aboCreateOrderWithAddon($fixture);
|
||||
$order = $orderData['order']->fresh('shopping_order_items.product');
|
||||
|
||||
$invoiceHtml = view('pdf.invoice-detail', ['shopping_order' => $order])->render();
|
||||
$deliveryHtml = view('pdf.delivery-detail', ['shopping_order' => $order])->render();
|
||||
|
||||
foreach ([$invoiceHtml, $deliveryHtml] as $html) {
|
||||
expect($html)->toContain(__('pdf.abo_items_heading'))
|
||||
->and($html)->toContain(__('pdf.abo_addon_items_heading'));
|
||||
expect(strpos($html, __('pdf.abo_items_heading')))
|
||||
->toBeLessThan(strpos($html, __('pdf.abo_addon_items_heading')));
|
||||
}
|
||||
});
|
||||
|
||||
it('überspringt Einmal-Artikel auch bei ensureUserAboItemsFromLatestOrder', function () {
|
||||
$fixture = aboCreateOneTimeFixture();
|
||||
$orderData = aboCreateOrderWithAddon($fixture);
|
||||
|
||||
$userAbo = UserAbo::create([
|
||||
'user_id' => $fixture['user']->id,
|
||||
'member_id' => $fixture['user']->id,
|
||||
'shopping_user_id' => $fixture['shoppingUser']->id,
|
||||
'email' => $fixture['shoppingUser']->billing_email,
|
||||
'is_for' => 'ot',
|
||||
'payone_userid' => 987654,
|
||||
'clearingtype' => 'cc',
|
||||
'status' => 1,
|
||||
'abo_interval' => 15,
|
||||
'next_date' => now()->addDays(20)->toDateString(),
|
||||
]);
|
||||
UserAboOrder::create([
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'shopping_order_id' => $orderData['order']->id,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
expect(AboHelper::ensureUserAboItemsFromLatestOrder($userAbo))->toBeTrue();
|
||||
|
||||
$items = UserAboItem::where('user_abo_id', $userAbo->id)->get();
|
||||
expect($items)->toHaveCount(1)
|
||||
->and((int) $items->first()->product_id)->toBe($orderData['aboProductId']);
|
||||
});
|
||||
129
tests/Feature/AboPortalCreateOneTimeTest.php
Normal file
129
tests/Feature/AboPortalCreateOneTimeTest.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Einmalprodukte im Portal-Abo-Wizard (Anlage, Variante A):
|
||||
* onetime_product_qty[] aus dem Upgrade-Schritt landet als getrennte Cart-Zeile
|
||||
* (Option abo_addon) im subscription-Yard; Basis-/Upgrade-Logik bleibt unberührt.
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Portal\AboController;
|
||||
use App\Models\Country;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShippingPrice;
|
||||
use App\Services\AboHelper;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function makePortalOneTimeEnv(): ShippingCountry
|
||||
{
|
||||
$country = Country::create([
|
||||
'code' => '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]);
|
||||
}
|
||||
|
||||
function callPortalAboMethod(string $method, array $args = [])
|
||||
{
|
||||
$controller = app(AboController::class);
|
||||
$reflection = new ReflectionMethod($controller, $method);
|
||||
|
||||
return $reflection->invoke($controller, ...$args);
|
||||
}
|
||||
|
||||
function findSubscriptionRows(int $productId, bool $addon): Collection
|
||||
{
|
||||
return Yard::instance('subscription')->search(function ($item) use ($productId, $addon) {
|
||||
return (int) $item->id === $productId && (bool) ($item->options->abo_addon ?? false) === $addon;
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->shippingCountry = makePortalOneTimeEnv();
|
||||
$yard = Yard::instance('subscription');
|
||||
$yard->destroy();
|
||||
$yard->setShippingCountryWithPrice($this->shippingCountry->id, 'abo-ot-customer');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Yard::instance('subscription')->destroy();
|
||||
});
|
||||
|
||||
it('übernimmt onetime_product_qty als getrennte Einmal-Cart-Zeilen', function () {
|
||||
$product = makeProduct(['3'], 25.50, 19);
|
||||
|
||||
request()->replace(['onetime_product_qty' => [$product->id => 2]]);
|
||||
callPortalAboMethod('oneTimeProductsToCart');
|
||||
|
||||
$rows = findSubscriptionRows($product->id, true);
|
||||
expect($rows)->toHaveCount(1)
|
||||
->and((int) $rows->first()->qty)->toBe(2)
|
||||
->and((bool) $rows->first()->options->abo_addon)->toBeTrue();
|
||||
});
|
||||
|
||||
it('ignoriert Produkte außerhalb des Einmal-Sortiments', function () {
|
||||
$aboOnlyProduct = makeProduct(['12'], 90, 19);
|
||||
|
||||
request()->replace(['onetime_product_qty' => [$aboOnlyProduct->id => 1]]);
|
||||
callPortalAboMethod('oneTimeProductsToCart');
|
||||
|
||||
expect(Yard::instance('subscription')->content())->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('entfernt mit Menge 0 nur die Einmal-Zeile, die Abo-Zeile bleibt', function () {
|
||||
$product = makeProduct(['3', '12'], 90, 19);
|
||||
|
||||
callPortalAboMethod('addProductToCart', [$product, 1]);
|
||||
request()->replace(['onetime_product_qty' => [$product->id => 2]]);
|
||||
callPortalAboMethod('oneTimeProductsToCart');
|
||||
|
||||
expect(findSubscriptionRows($product->id, true))->toHaveCount(1)
|
||||
->and(findSubscriptionRows($product->id, false))->toHaveCount(1);
|
||||
|
||||
request()->replace(['onetime_product_qty' => [$product->id => 0]]);
|
||||
callPortalAboMethod('oneTimeProductsToCart');
|
||||
|
||||
expect(findSubscriptionRows($product->id, true))->toHaveCount(0)
|
||||
->and(findSubscriptionRows($product->id, false))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('lässt die Basis-/Upgrade-Logik Einmal-Zeilen unangetastet', function () {
|
||||
$product = makeProduct(['3', '13'], 50, 19);
|
||||
|
||||
request()->replace(['onetime_product_qty' => [$product->id => 3]]);
|
||||
callPortalAboMethod('oneTimeProductsToCart');
|
||||
|
||||
callPortalAboMethod('addProductToCart', [$product, 1]);
|
||||
|
||||
$addonRows = findSubscriptionRows($product->id, true);
|
||||
$regularRows = findSubscriptionRows($product->id, false);
|
||||
|
||||
expect($addonRows)->toHaveCount(1)
|
||||
->and((int) $addonRows->first()->qty)->toBe(3)
|
||||
->and($regularRows)->toHaveCount(1)
|
||||
->and((int) $regularRows->first()->qty)->toBe(1);
|
||||
});
|
||||
|
||||
it('zählt Einmal-Zeilen nicht als Basis-Produkt', function () {
|
||||
$addonWithBaseShowOn = makeProduct(['3', '12'], 90, 19);
|
||||
|
||||
request()->replace(['onetime_product_qty' => [$addonWithBaseShowOn->id => 1]]);
|
||||
callPortalAboMethod('oneTimeProductsToCart');
|
||||
|
||||
expect(AboHelper::aboHasBaseProduct(Yard::instance('subscription')->getContentByOrder()))->toBeFalse();
|
||||
|
||||
$baseProduct = makeProduct(['12'], 90, 19);
|
||||
callPortalAboMethod('addProductToCart', [$baseProduct, 1]);
|
||||
|
||||
expect(AboHelper::aboHasBaseProduct(Yard::instance('subscription')->getContentByOrder()))->toBeTrue();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue