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>
129 lines
4.7 KiB
PHP
129 lines
4.7 KiB
PHP
<?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();
|
|
});
|