mivita/tests/Feature/AboCreateOneTimeItemsTest.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

274 lines
9.5 KiB
PHP

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