diff --git a/resources/views/admin/abo/_order_onetime_show.blade.php b/resources/views/admin/abo/_order_onetime_show.blade.php
index ebdc231..376e2e6 100644
--- a/resources/views/admin/abo/_order_onetime_show.blade.php
+++ b/resources/views/admin/abo/_order_onetime_show.blade.php
@@ -2,11 +2,13 @@
$one_time_items = $user_abo->one_time_items()->with('product')->get();
$hasOneTimeChanges = \App\Services\AboOneTimeService::hasUnconfirmedChanges($user_abo);
$hasConfirmedOneTimeItems = \App\Services\AboOneTimeService::hasConfirmedItems($user_abo);
- $oneTimeGross = $summary['one_time']['gross'] ?? 0;
+ $hasBindingConfirmed = \App\Services\AboOneTimeService::confirmedItems($user_abo)->isNotEmpty();
+ $confirmedGross = $summary['one_time']['gross'] ?? 0;
+ $pendingGross = \App\Services\AboOneTimeService::pendingGross($user_abo);
$nextDeliveryTotal = $summary['total_with_shipping'] ?? 0;
$oneTimeConfirmationState = $hasOneTimeChanges ? 'changed' : ($hasConfirmedOneTimeItems ? 'confirmed' : 'empty');
@endphp
-@if(isset($error_message) && $error_message)
+@if (isset($error_message) && $error_message)
-@if($oneTimeGross > 0 || $hasOneTimeChanges || $hasConfirmedOneTimeItems)
-
+@if ($one_time_items->count() > 0)
+
@endif
diff --git a/tests/Feature/AboOneTimeServiceTest.php b/tests/Feature/AboOneTimeServiceTest.php
index 1325120..7a27841 100644
--- a/tests/Feature/AboOneTimeServiceTest.php
+++ b/tests/Feature/AboOneTimeServiceTest.php
@@ -129,6 +129,71 @@ describe('AboOneTimeService', function () {
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 () {
diff --git a/tests/Feature/AboOneTimeViewTest.php b/tests/Feature/AboOneTimeViewTest.php
index bdcd4ff..e7a3d0a 100644
--- a/tests/Feature/AboOneTimeViewTest.php
+++ b/tests/Feature/AboOneTimeViewTest.php
@@ -9,7 +9,7 @@ uses(Tests\TestCase::class, RefreshDatabase::class);
describe('Abo Einmal-Produkte Views', function () {
beforeEach(fn () => makeShopEnv());
- it('rendert die Einmal-Produktliste mit Position und Zwischensumme', function () {
+ it('rendert die Einmal-Produktliste mit Position und offener Zwischensumme', function () {
$abo = makeMeAbo();
$product = makeProduct(['2'], 119, 19);
@@ -21,7 +21,7 @@ describe('Abo Einmal-Produkte Views', function () {
'price_net' => 100.0,
'tax_rate' => 19.0,
'tax' => 19.0,
- 'status' => 1,
+ 'status' => 0,
]);
$summary = ['one_time' => ['gross' => 238.0, 'net' => 200.0, 'tax' => 38.0]];
@@ -32,10 +32,50 @@ describe('Abo Einmal-Produkte Views', function () {
])->render();
expect($html)->toContain($product->getLang('name'))
- ->and($html)->toContain(__('abo.onetime_subtotal'))
+ ->and($html)->toContain(__('abo.onetime_pending_subtotal'))
+ ->and($html)->toContain(__('abo.onetime_status_pending'))
->and($html)->toContain('238,00');
});
+ it('zeigt bestätigte und offene Artikel mit getrennten Zwischensummen', 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,
+ ]);
+
+ $html = view('admin.abo._order_onetime_show', [
+ 'user_abo' => $abo->fresh(),
+ 'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
+ ])->render();
+
+ expect($html)->toContain(__('abo.onetime_confirmed_subtotal'))
+ ->and($html)->toContain(__('abo.onetime_pending_subtotal'))
+ ->and($html)->toContain(__('abo.onetime_new_total'))
+ ->and($html)->toContain(__('abo.onetime_status_confirmed'))
+ ->and($html)->toContain(__('abo.onetime_status_pending'))
+ ->and($html)->toContain(__('abo.onetime_pending_hint'))
+ ->and($html)->toContain('50,00')
+ ->and($html)->toContain('169,00');
+ });
+
it('zeigt bei unbestätigten Einmal-Artikeln die Bestätigungsbuttons', function () {
$abo = makeMeAbo();
$product = makeProduct(['2'], 119, 19);
@@ -56,7 +96,7 @@ describe('Abo Einmal-Produkte Views', function () {
'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
])->render();
- expect($html)->toContain(__('abo.onetime_next_delivery_total'))
+ expect($html)->toContain(__('abo.onetime_pending_subtotal'))
->and($html)->toContain(__('abo.onetime_legal_notice', [
'nextBillingDate' => $abo->next_date,
'agb' => '
'.__('abo.confirm_terms_link').'',
diff --git a/tests/Feature/AboOneTimeWindowTest.php b/tests/Feature/AboOneTimeWindowTest.php
index 11517dc..b1bacc2 100644
--- a/tests/Feature/AboOneTimeWindowTest.php
+++ b/tests/Feature/AboOneTimeWindowTest.php
@@ -5,6 +5,7 @@ use App\Models\ShoppingOrderItem;
use App\Models\UserAbo;
use App\Models\UserAboOneTimeItem;
use App\Services\AboHelper;
+use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -82,6 +83,48 @@ describe('isOneTimeWindowOpen', function () {
});
});
+describe('isOneTimeFeatureVisible', function () {
+ it('ist sichtbar, wenn das Fenster offen ist und ein VIP-User (Admin) eingeloggt ist', function () {
+ $vip = new User;
+ $vip->admin = 1;
+ $this->actingAs($vip, 'user');
+
+ $abo = new UserAbo;
+ $abo->next_date = now()->addDays(2); // innerhalb des 4-Tage-Fensters
+
+ expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeTrue();
+ });
+
+ it('ist nicht sichtbar für eingeloggte Nicht-VIP-User', function () {
+ $user = new User;
+ $user->admin = 0;
+ $this->actingAs($user, 'user');
+
+ $abo = new UserAbo;
+ $abo->next_date = now()->addDays(2);
+
+ expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeFalse();
+ });
+
+ it('ist nicht sichtbar ohne User auf dem user-Guard (Portal/Endkunde)', function () {
+ $abo = new UserAbo;
+ $abo->next_date = now()->addDays(2);
+
+ expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeFalse();
+ });
+
+ it('ist nicht sichtbar bei geschlossenem Fenster, auch für VIP-User', function () {
+ $vip = new User;
+ $vip->admin = 1;
+ $this->actingAs($vip, 'user');
+
+ $abo = new UserAbo;
+ $abo->next_date = now()->addDays(30); // außerhalb des Fensters
+
+ expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeFalse();
+ });
+});
+
describe('UserAboOneTimeItem Model', function () {
it('definiert die Beziehung one_time_items auf UserAbo', function () {
$relation = (new UserAbo)->one_time_items();