service = new AboOneTimeService; }); it('fügt ein erlaubtes Produkt (show_on 2) mit Preis-Snapshot hinzu', function () { $abo = makeMeAbo(); $product = makeProduct(['2']); $message = $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); expect($message)->toBeNull(); $item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first(); expect($item)->not->toBeNull() ->and($item->qty)->toBe(1) ->and((float) $item->price)->toBeGreaterThan(0.0) ->and((float) $item->tax_rate)->toBe(19.0) ->and((float) $item->price_net)->toBeGreaterThan(0.0); }); it('erhöht die Menge, wenn dasselbe Produkt erneut hinzugefügt wird', function () { $abo = makeMeAbo(); $product = makeProduct(['2']); $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); $item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first(); expect(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(1) ->and($item->qty)->toBe(2); }); it('lehnt Produkte aus dem Abo-Sortiment (show_on 12) ab', function () { $abo = makeMeAbo(); $product = makeProduct(['12']); $message = $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); expect($message)->toBe(__('abo.onetime_product_not_allowed')) ->and(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(0); }); it('aktualisiert die Menge eines Einmal-Artikels (geclamped)', function () { $abo = makeMeAbo(); $product = makeProduct(['2']); $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); $item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first(); $this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 5]); expect($item->fresh()->qty)->toBe(5); $this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 999]); expect($item->fresh()->qty)->toBe(100); }); it('entfernt einen Einmal-Artikel', function () { $abo = makeMeAbo(); $product = makeProduct(['2']); $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); $item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first(); $message = $this->service->handleAction($abo, ['action' => 'remove', 'one_time_item_id' => $item->id]); expect($message)->toBeNull() ->and(UserAboOneTimeItem::where('user_abo_id', $abo->id)->count())->toBe(0); }); it('bestätigt den aktuellen Stand der Einmal-Artikel', function () { $abo = makeMeAbo(); $product = makeProduct(['2']); $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); $message = $this->service->handleAction($abo, ['action' => 'confirm']); $item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first(); expect($message)->toBeNull() ->and($item->confirmed_qty)->toBe(1) ->and($item->confirmed_at)->not->toBeNull() ->and($item->status)->toBe(1) ->and(AboOneTimeService::hasUnconfirmedChanges($abo))->toBeFalse() ->and(AboOneTimeService::hasConfirmedItems($abo))->toBeTrue(); }); it('verwirft unbestätigte Änderungen und stellt den bestätigten Stand wieder her', function () { $abo = makeMeAbo(); $product = makeProduct(['2']); $this->service->handleAction($abo, ['action' => 'add', 'product_id' => $product->id]); $this->service->handleAction($abo, ['action' => 'confirm']); $item = UserAboOneTimeItem::where('user_abo_id', $abo->id)->first(); $this->service->handleAction($abo, ['action' => 'update', 'one_time_item_id' => $item->id, 'qty' => 4]); expect(AboOneTimeService::hasUnconfirmedChanges($abo))->toBeTrue(); $message = $this->service->handleAction($abo, ['action' => 'discard']); expect($message)->toBeNull() ->and($item->fresh()->qty)->toBe(1) ->and($item->fresh()->status)->toBe(1) ->and(AboOneTimeService::hasUnconfirmedChanges($abo))->toBeFalse(); }); it('verhindert das Bearbeiten fremder Einmal-Artikel', function () { $abo = makeMeAbo(); $foreignAbo = makeMeAbo(); $product = makeProduct(['2']); $this->service->handleAction($foreignAbo, ['action' => 'add', 'product_id' => $product->id]); $foreignItem = UserAboOneTimeItem::where('user_abo_id', $foreignAbo->id)->first(); $message = $this->service->handleAction($abo, ['action' => 'remove', 'one_time_item_id' => $foreignItem->id]); expect($message)->toBe(__('abo.abo_item_not_found')) ->and(UserAboOneTimeItem::find($foreignItem->id))->not->toBeNull(); }); it('liefert eine Fehlermeldung bei ungültiger Aktion', function () { $abo = makeMeAbo(); expect($this->service->handleAction($abo, ['action' => 'foo']))->toBe(__('abo.onetime_action_invalid')); }); it('trennt bestätigte und offene Einmal-Artikel inkl. offener Zwischensumme', function () { $abo = makeMeAbo(); $confirmed = makeProduct(['2'], 119, 19); $pending = makeProduct(['2'], 50, 19); UserAboOneTimeItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $confirmed->id, 'qty' => 1, 'confirmed_qty' => 1, 'confirmed_at' => now(), 'price' => 119.0, 'tax_rate' => 19.0, 'status' => 1, ]); UserAboOneTimeItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $pending->id, 'qty' => 2, 'price' => 50.0, 'tax_rate' => 19.0, 'status' => 0, ]); expect(AboOneTimeService::confirmedItems($abo)->count())->toBe(1) ->and(AboOneTimeService::pendingItems($abo)->count())->toBe(1) ->and(AboOneTimeService::pendingGross($abo))->toBe(100.0); }); it('behandelt einen geänderten bestätigten Artikel als offen', function () { $abo = makeMeAbo(); $product = makeProduct(['2'], 119, 19); UserAboOneTimeItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $product->id, 'qty' => 3, 'confirmed_qty' => 1, 'confirmed_at' => now(), 'price' => 119.0, 'tax_rate' => 19.0, 'status' => 0, ]); expect(AboOneTimeService::confirmedItems($abo)->count())->toBe(0) ->and(AboOneTimeService::pendingItems($abo)->count())->toBe(1) ->and(AboOneTimeService::pendingGross($abo))->toBe(357.0); }); }); describe('AboOrderCart::addOneTimeItemsToYard', function () { beforeEach(fn () => makeShopEnv()); it('lädt nur verbindlich bestätigte Einmal-Artikel in den Warenkorb', function () { $abo = makeMeAbo(); $confirmed = makeProduct(['2'], 119, 19); $pending = makeProduct(['2'], 50, 19); UserAboOneTimeItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $confirmed->id, 'qty' => 1, 'confirmed_qty' => 1, 'confirmed_at' => now(), 'price' => 119.0, 'tax_rate' => 19.0, 'status' => 1, ]); UserAboOneTimeItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $pending->id, 'qty' => 1, 'price' => 50.0, 'tax_rate' => 19.0, 'status' => 0, ]); $yard = Yard::instance(AboOrderCart::INSTANCE); $yard->destroy(); AboOrderCart::addOneTimeItemsToYard($abo->fresh()); expect($yard->content()->count())->toBe(1) ->and((int) $yard->content()->first()->id)->toBe($confirmed->id); $yard->destroy(); }); }); describe('AboOrderCart::buildOneTimeSnapshot', function () { beforeEach(fn () => makeShopEnv()); it('berechnet einen konsistenten Brutto/Netto/Steuer-Snapshot', function () { $abo = makeMeAbo(); $product = makeProduct(['2'], 119, 19); $snapshot = AboOrderCart::buildOneTimeSnapshot($product, $abo); expect($snapshot['price'])->toBe(119.0) ->and($snapshot['tax_rate'])->toBe(19.0) ->and(round($snapshot['price_net'], 2))->toBe(100.0) ->and(round($snapshot['tax'], 2))->toBe(19.0) ->and($snapshot['points'])->toBe(7); }); }); describe('AboOrderCart::getSplitSummary', function () { it('trennt Abo- und Einmal-Artikel-Summen anhand der abo_addon-Option', function () { $yard = Yard::instance(AboOrderCart::INSTANCE); $yard->destroy(); $aboRow = $yard->add(1, 'Abo-Produkt', 1, 119.0, false, false, ['weight' => 100]); Yard::setTax($aboRow->rowId, 19.0); $oneTimeRow = $yard->add(2, 'Einmal-Produkt', 2, 59.5, false, false, ['weight' => 100, 'abo_addon' => true]); Yard::setTax($oneTimeRow->rowId, 19.0); $summary = AboOrderCart::getSplitSummary(); expect($summary['abo']['gross'])->toBe(119.0) ->and(round($summary['abo']['net'], 2))->toBe(100.0) ->and($summary['one_time']['gross'])->toBe(119.0) ->and($summary['has_one_time'])->toBeTrue(); $yard->destroy(); }); }); describe('AboOneTimeItemRequest', function () { it('definiert die erwarteten Validierungsregeln', function () { $rules = (new AboOneTimeItemRequest)->rules(); expect($rules)->toHaveKeys(['action', 'product_id', 'one_time_item_id', 'qty']) ->and($rules['action'])->toContain('required'); }); });