From ac870a4076ff17e911e85f9cc0282e3dfad8c15a Mon Sep 17 00:00:00 2001
From: Kevin Adametz
Date: Thu, 9 Jul 2026 15:34:28 +0000
Subject: [PATCH] Buchungsseite: Buchungs- und Verlaengerungsdatum des Abos aus
Stripe
Cashier spiegelt die Periode nicht lokal; neuer SubscriptionPeriodResolver
holt start_date + current_period_end (Item-Ebene) gecacht, Fallback null.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../Billing/SubscriptionPeriodResolver.php | 47 ++++++++++++
.../livewire/customer/bookings.blade.php | 22 ++++++
tests/Feature/Billing/BookingsPageTest.php | 71 +++++++++++++++++++
3 files changed, 140 insertions(+)
create mode 100644 app/Services/Billing/SubscriptionPeriodResolver.php
diff --git a/app/Services/Billing/SubscriptionPeriodResolver.php b/app/Services/Billing/SubscriptionPeriodResolver.php
new file mode 100644
index 00000000..1a65e4bb
--- /dev/null
+++ b/app/Services/Billing/SubscriptionPeriodResolver.php
@@ -0,0 +1,47 @@
+stripe_id}:".($subscription->updated_at?->timestamp ?? 0);
+
+ return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($subscription): ?array {
+ try {
+ $stripe = $subscription->asStripeSubscription();
+ $renewsAt = $stripe->items->data[0]->current_period_end ?? null;
+
+ if (! $renewsAt) {
+ return null;
+ }
+
+ return [
+ 'booked_at' => Carbon::createFromTimestamp($stripe->start_date),
+ 'renews_at' => Carbon::createFromTimestamp($renewsAt),
+ ];
+ } catch (Throwable) {
+ return null;
+ }
+ });
+ }
+}
diff --git a/resources/views/livewire/customer/bookings.blade.php b/resources/views/livewire/customer/bookings.blade.php
index 74dcac7f..c059d331 100644
--- a/resources/views/livewire/customer/bookings.blade.php
+++ b/resources/views/livewire/customer/bookings.blade.php
@@ -14,6 +14,7 @@ use App\Services\Billing\CreditPricingService;
use App\Services\Billing\CreditTopupService;
use App\Services\Billing\ExtraPmPurchaseService;
use App\Services\Billing\StripeCheckoutService;
+use App\Services\Billing\SubscriptionPeriodResolver;
use App\Services\Billing\VatResolver;
use Flux\Flux;
use Livewire\Attributes\Layout;
@@ -257,6 +258,12 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
$currentInterval = $subscription->stripe_price === $currentPlan->stripe_price_id_yearly ? 'yearly' : 'monthly';
}
+ // Buchungs- und Verlängerungsdatum des laufenden Abos (Stripe spiegelt
+ // die Periode nicht lokal, daher gecachter Abruf; null bei Fehler).
+ $subscriptionPeriod = $subscription
+ ? app(SubscriptionPeriodResolver::class)->resolve($subscription)
+ : null;
+
// Vorgemerkter Tarifwechsel (Downgrade zum Periodenende): lokale
// Spiegelung der Stripe-Schedule-Vormerkung für die Anzeige.
$scheduledChangePlan = null;
@@ -277,6 +284,7 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
'currentPlan' => $currentPlan,
'currentInterval' => $currentInterval,
'subscription' => $subscription,
+ 'subscriptionPeriod' => $subscriptionPeriod,
'scheduledChangePlan' => $scheduledChangePlan,
'scheduledChangeAt' => $subscription?->scheduled_change_at
? \Illuminate\Support\Carbon::parse($subscription->scheduled_change_at)
@@ -531,6 +539,20 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
{{ __('max. :limit Veröffentlichungen pro Tag', ['limit' => $currentPlan->daily_limit]) }}
@endif
+ @if ($subscriptionPeriod)
+
+
+
+ {{ __('Gebucht am :date', ['date' => $subscriptionPeriod['booked_at']->format('d.m.Y')]) }}
+
+ @unless ($subscription?->onGracePeriod())
+
+
+ {{ __('Verlängerung am :date', ['date' => $subscriptionPeriod['renews_at']->format('d.m.Y')]) }}
+
+ @endunless
+
+ @endif
@if ($subscription?->onGracePeriod())
{{ __('Gekündigt — läuft am :date aus.', ['date' => $subscription->ends_at?->format('d.m.Y')]) }}
diff --git a/tests/Feature/Billing/BookingsPageTest.php b/tests/Feature/Billing/BookingsPageTest.php
index 89f3786e..d1921005 100644
--- a/tests/Feature/Billing/BookingsPageTest.php
+++ b/tests/Feature/Billing/BookingsPageTest.php
@@ -12,14 +12,25 @@ use App\Models\User;
use App\Models\UserPaymentOption;
use App\Services\Billing\CreditWalletService;
use App\Services\Billing\StripeCheckoutService;
+use App\Services\Billing\SubscriptionPeriodResolver;
use Database\Seeders\LegalDocumentSeeder;
use Database\Seeders\RolesAndPermissionsSeeder;
+use Illuminate\Support\Carbon;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
+
+ // Standard: kein echter Stripe-Abruf fürs Buchungs-/Verlängerungsdatum.
+ // Die Datums-Tests binden bei Bedarf einen eigenen Stub.
+ $this->instance(
+ SubscriptionPeriodResolver::class,
+ Mockery::mock(SubscriptionPeriodResolver::class, function ($mock) {
+ $mock->shouldReceive('resolve')->andReturnNull();
+ })
+ );
});
function bookingsTestCustomer(): User
@@ -448,3 +459,63 @@ test('a scheduled plan change is shown on the current plan card', function () {
->assertSee('Tarifwechsel zu Kleiner Tarif am')
->assertSee(now()->addDays(9)->format('d.m.Y'));
});
+
+test('the current plan card shows booking and renewal dates from stripe', function () {
+ /** @var TestCase $this */
+ $user = bookingsTestCustomer();
+ BillingAddress::factory()->create(['user_id' => $user->id]);
+
+ $plan = Plan::factory()->create([
+ 'name' => 'Pro',
+ 'slug' => 'pro',
+ 'monthly_price_cents' => 9900,
+ 'stripe_price_id_monthly' => 'price_test_pro_m',
+ ]);
+ subscribeUserToPlan($user, $plan);
+
+ // Stripe-Periode deterministisch stubben — kein echter API-Abruf im Test.
+ $this->instance(
+ SubscriptionPeriodResolver::class,
+ Mockery::mock(SubscriptionPeriodResolver::class, function ($mock) {
+ $mock->shouldReceive('resolve')->andReturn([
+ 'booked_at' => Carbon::parse('2026-07-09'),
+ 'renews_at' => Carbon::parse('2026-08-09'),
+ ]);
+ })
+ );
+
+ $this->actingAs($user);
+
+ LivewireVolt::test('customer.bookings')
+ ->assertSee('Aktueller Tarif')
+ ->assertSee('Gebucht am 09.07.2026')
+ ->assertSee('Verlängerung am 09.08.2026');
+});
+
+test('the current plan card omits the dates when the stripe period is unavailable', function () {
+ /** @var TestCase $this */
+ $user = bookingsTestCustomer();
+ BillingAddress::factory()->create(['user_id' => $user->id]);
+
+ $plan = Plan::factory()->create([
+ 'name' => 'Pro',
+ 'slug' => 'pro',
+ 'stripe_price_id_monthly' => 'price_test_pro_m2',
+ ]);
+ subscribeUserToPlan($user, $plan);
+
+ // Stripe nicht erreichbar → Resolver liefert null; Seite rendert trotzdem.
+ $this->instance(
+ SubscriptionPeriodResolver::class,
+ Mockery::mock(SubscriptionPeriodResolver::class, function ($mock) {
+ $mock->shouldReceive('resolve')->andReturnNull();
+ })
+ );
+
+ $this->actingAs($user);
+
+ LivewireVolt::test('customer.bookings')
+ ->assertSee('Aktueller Tarif')
+ ->assertDontSee('Gebucht am')
+ ->assertDontSee('Verlängerung am');
+});