From a0547208d3b290d35ab59820cd3098ec6bf68669 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 16 Jun 2026 16:37:16 +0000 Subject: [PATCH] =?UTF-8?q?WS-5:=20Buchungs-Gate=20launch-ready=20?= =?UTF-8?q?=E2=80=93=20proaktiver=20Dashboard-Hinweis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Abo-/Bestandsschutz-Gate (User::hasActiveBooking, BookingRequiredException, Submit-Modal, API 402/422) war bereits implementiert und getestet. Ergänzt wird die proaktive Block-UX: - Kunden-Dashboard zeigt bei scharfem Gate (billing.enforce_booking=true) und fehlender aktiver Buchung einen Banner "Buchung erforderlich, um zu veröffentlichen" mit Link zur Buchungsseite. Bestandskunden (grandfathered) sehen den Hinweis nicht; bei offenem Gate bleibt er unsichtbar. - Tests: Banner aus (Gate offen) / an (Gate scharf, keine Buchung) / Ausnahme für grandfathered. Launch-Flip bleibt env-gesteuert (BILLING_ENFORCE_BOOKING), Dev-Default false – .env.example und Detailplan (WS-5 ✅) dokumentieren den Schalter. Co-Authored-By: Claude Opus 4.8 --- .env.example | 6 +++ ...Link, Compliance, Bestandsschutz, Auth).md | 13 +++-- .../livewire/customer/dashboard.blade.php | 31 ++++++++++++ tests/Feature/Customer/DashboardTest.php | 48 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 6eae48f..6de965c 100644 --- a/.env.example +++ b/.env.example @@ -72,3 +72,9 @@ VITE_APP_NAME="${APP_NAME}" GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT_URI=https://pressekonto.com/auth/google/callback + +# Abo-/Bestandsschutz-Gate (WS-5). Ist der Schalter aus, darf jeder einreichen +# (Dev-Default). Für den Launch auf true setzen: Einreichen zur Prüfung erfordert +# dann ein aktives Abo, einen bezahlten Einzelkauf oder Bestandsschutz +# (grandfathered). Siehe User::hasActiveBooking(). +BILLING_ENFORCE_BOOKING=false diff --git a/docs/weiteres/Detailplan Umsetzung Launch-Slice (Magic-Link, Compliance, Bestandsschutz, Auth).md b/docs/weiteres/Detailplan Umsetzung Launch-Slice (Magic-Link, Compliance, Bestandsschutz, Auth).md index 63d31c5..6a2fe2f 100644 --- a/docs/weiteres/Detailplan Umsetzung Launch-Slice (Magic-Link, Compliance, Bestandsschutz, Auth).md +++ b/docs/weiteres/Detailplan Umsetzung Launch-Slice (Magic-Link, Compliance, Bestandsschutz, Auth).md @@ -92,11 +92,16 @@ Alle hinter WS-2 **oder** regulärem Login. - **Tests:** gemeinsame Abrechnung zweier Posten; portalübergreifende Geltung; Worker erzeugt Rechnung zum Periodenende. - **Aufwand:** ~2–3 Tage. Parallel zu WS-1/2 möglich. -### WS-5 — Abo-/Bestandsschutz-Gate scharf schalten (Merkliste 6) +### WS-5 — Abo-/Bestandsschutz-Gate scharf schalten (Merkliste 6) — ✅ erledigt 16.06.2026 **Scope:** „User ohne aktives Abo oder Bestandsschutz dürfen keine PM schreiben." -- Mechanik existiert (`hasActiveBooking()` inkl. `grandfathered`, `BookingRequiredException`). Nötig: `billing.enforce_booking` für Launch auf `true`, Block-UX prüfen, Bestandsschutz-Fall absichern. -- **Tests:** User ohne Buchung → blockiert; grandfathered → erlaubt; aktives Abo → erlaubt. -- **Aufwand:** ~½–1 Tag. **Abhängigkeit:** WS-4. +- Mechanik existiert (`hasActiveBooking()` inkl. `grandfathered`, `BookingRequiredException`) und ist mit `enforce_booking=true` voll getestet: + - **Backend-Gate:** `PressReleaseService::submitForReview()` wirft `BookingRequiredException`/`QuotaExceededException`. + - **API:** `POST /api/v1/press-releases/{id}/submit` → `402` (keine Buchung) bzw. `422` (Kontingent erschöpft). + - **Block-UX am Einreichpunkt:** `press-release-submit-modal.blade.php` zeigt ohne Buchung statt des Prüf-Flows den Hinweis „Buchung erforderlich" + Button zur Buchungsseite; alle Volt-Caller (show/edit/create/index) fangen die Exceptions und zeigen einen Toast. + - **Proaktiver Hinweis:** Kunden-Dashboard zeigt bei scharfem Gate ohne aktive Buchung einen Banner „Buchung erforderlich, um zu veröffentlichen" → `me.bookings.index`. Bestandskunden (grandfathered) sehen ihn nicht. +- **Launch-Flip:** Kein Code-Default ändern — der Schalter bleibt in `config/billing.php` auf `false` (Dev-Default). Scharfschalten ausschließlich per Prod-Env `BILLING_ENFORCE_BOOKING=true` (→ WS-7-Checkliste). +- **Tests:** `HasActiveBookingTest`, `PressReleaseSubmitApiTest` (402/422), `DashboardTest` (Banner an/aus + grandfathered-Ausnahme), `LegacyBundleTest` (grandfathered zählt portalübergreifend). +- **Aufwand:** erledigt. **Abhängigkeit:** WS-4 (erledigt). ### WS-6 — Login/Registrierung: Flow + E-Mail-Verifizierung + Google-Login (Merkliste 7 + Zweig 3) **Scope nach Entscheidung 2 & 3:** diff --git a/resources/views/livewire/customer/dashboard.blade.php b/resources/views/livewire/customer/dashboard.blade.php index afdc679..7179790 100644 --- a/resources/views/livewire/customer/dashboard.blade.php +++ b/resources/views/livewire/customer/dashboard.blade.php @@ -56,6 +56,10 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C return [ 'user' => $user, 'selectedCompany' => $selectedCompany, + // Launch-Gate (WS-5): Solange billing.enforce_booking aus ist, ist + // dies immer false und der Hinweis bleibt unsichtbar. Bei scharfem + // Gate sehen Bestandskunden (grandfathered) den Banner nicht. + 'bookingRequired' => config('billing.enforce_booking') && ! $user->hasActiveBooking(), 'stats' => [ 'total' => (int) ($stats->total ?? 0), 'published' => (int) ($stats->published ?? 0), @@ -236,6 +240,33 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C + {{-- ============== BUCHUNGS-GATE (WS-5) ============== --}} + @if ($bookingRequired) +
+
+
+ + + +
+
+ {{ __('Buchung erforderlich, um zu veröffentlichen') }} +
+

+ {{ __('Sie können Pressemitteilungen wie gewohnt erstellen und speichern. Zum Einreichen zur Prüfung wird eine aktive Buchung benötigt.') }} +

+
+
+
+ + {{ __('Buchung auswählen') }} + +
+
+
+ @endif + {{-- ============== STAT-CARDS — KPI-Reihe ============== --}}
diff --git a/tests/Feature/Customer/DashboardTest.php b/tests/Feature/Customer/DashboardTest.php index 224d729..47229e8 100644 --- a/tests/Feature/Customer/DashboardTest.php +++ b/tests/Feature/Customer/DashboardTest.php @@ -1,11 +1,13 @@ assertSee('33'); }); +test('the booking-required banner stays hidden while the gate is open', function () { + /** @var TestCase $this */ + config()->set('billing.enforce_booking', false); + + $customer = User::factory()->create(['is_active' => true]); + $customer->assignRole('customer'); + + $this->actingAs($customer); + + LivewireVolt::test('customer.dashboard') + ->assertDontSee(__('Buchung erforderlich, um zu veröffentlichen')); +}); + +test('the booking-required banner appears when the gate is enforced and no booking exists', function () { + /** @var TestCase $this */ + config()->set('billing.enforce_booking', true); + + $customer = User::factory()->create(['is_active' => true]); + $customer->assignRole('customer'); + + $this->actingAs($customer); + + LivewireVolt::test('customer.dashboard') + ->assertSee(__('Buchung erforderlich, um zu veröffentlichen')) + ->assertSee(route('me.bookings.index'), false); +}); + +test('a grandfathered bestandskunde does not see the booking-required banner', function () { + /** @var TestCase $this */ + config()->set('billing.enforce_booking', true); + + $customer = User::factory()->create(['is_active' => true]); + $customer->assignRole('customer'); + + UserPaymentOption::factory()->create([ + 'user_id' => $customer->id, + 'status' => UserPaymentOptionStatus::Grandfathered->value, + 'stripe_subscription_id' => null, + ]); + + $this->actingAs($customer); + + LivewireVolt::test('customer.dashboard') + ->assertDontSee(__('Buchung erforderlich, um zu veröffentlichen')); +}); + test('billing address hint disappears once address is complete', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]);