- Live-Review-Gate: Einmalprodukte nur fuer VIP im Sales Center sichtbar, Portal ausgeblendet (AboHelper::isOneTimeFeatureVisible + Gates in Controllern) - Nur verbindlich bestaetigte Einmal-Artikel fliessen in die Lieferung; Service-Helfer confirmedItems/pendingItems/pendingGross - Footer-Layout der Einmalprodukt-Liste: bestaetigte Summe + Gesamtbetrag, Trennstrich, offener Betrag und neue Gesamtsumme (dunkelgruen) - Uebersetzungen DE/EN/ES/FR (onetime_new_total u.a.), Tests angepasst/ergaenzt Co-authored-by: Cursor <cursoragent@cursor.com>
245 lines
10 KiB
PHP
245 lines
10 KiB
PHP
<?php
|
|
|
|
use App\Http\Requests\Abo\AboOneTimeItemRequest;
|
|
use App\Models\UserAboOneTimeItem;
|
|
use App\Services\AboOneTimeService;
|
|
use App\Services\AboOrderCart;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(Tests\TestCase::class, RefreshDatabase::class);
|
|
|
|
describe('AboOneTimeService', function () {
|
|
beforeEach(function () {
|
|
makeShopEnv();
|
|
$this->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');
|
|
});
|
|
});
|