mivita/tests/Feature/AboPortalCreateOneTimeTest.php
Kevin cdb121fde0 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>
2026-07-21 14:36:40 +00:00

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();
});