billing: Abo-Kündigungen aus dem Stripe-Portal im Aktivitäts-Verlauf

Kündigung zum Periodenende, deren Rücknahme (customer.subscription.updated,
Transition über previous_attributes.cancel_at_period_end) und das endgültige
Abo-Ende (customer.subscription.deleted) erscheinen jetzt als neuer Typ
"Abo" unter Admin ▸ Reports ▸ Verlauf — bisher war dort nur die Zahlung
sichtbar, die Portal-Kündigung blieb unsichtbar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-20 10:50:47 +00:00
parent 2765afec2f
commit 3842fbcc15
3 changed files with 180 additions and 2 deletions

View file

@ -1,9 +1,11 @@
<?php
use App\Enums\ActivityType;
use App\Enums\InvoiceStatus;
use App\Enums\SinglePurchaseStatus;
use App\Listeners\ProcessStripeWebhook;
use App\Mail\BookingPaymentConfirmed;
use App\Models\ActivityEvent;
use App\Models\BillingAddress;
use App\Models\Invoice;
use App\Models\SinglePurchase;
@ -354,3 +356,83 @@ test('a subscription update to a different price keeps the scheduled downgrade',
expect($subscription->fresh()->scheduled_stripe_price)->toBe('price_new');
});
test('a portal cancellation at period end is recorded in the activity log', function () {
$user = User::factory()->create(['stripe_id' => 'cus_test_1']);
$user->subscriptions()->create([
'type' => 'default',
'stripe_id' => 'sub_cancel_1',
'stripe_status' => 'active',
'stripe_price' => 'price_x',
'quantity' => 1,
]);
app(ProcessStripeWebhook::class)->handle(new WebhookReceived([
'type' => 'customer.subscription.updated',
'data' => [
'object' => [
'id' => 'sub_cancel_1',
'customer' => 'cus_test_1',
'cancel_at_period_end' => true,
'cancel_at' => now()->addMonth()->timestamp,
],
'previous_attributes' => ['cancel_at_period_end' => false],
],
]));
$activityEvent = ActivityEvent::sole();
expect($activityEvent->type)->toBe(ActivityType::SubscriptionChange)
->and($activityEvent->user_id)->toBe($user->id)
->and($activityEvent->description)->toContain('Abo gekündigt')
->and($activityEvent->description)->toContain(now()->addMonth()->format('d.m.Y'));
});
test('resuming a cancelled subscription is recorded in the activity log', function () {
User::factory()->create(['stripe_id' => 'cus_test_1']);
app(ProcessStripeWebhook::class)->handle(new WebhookReceived([
'type' => 'customer.subscription.updated',
'data' => [
'object' => [
'id' => 'sub_cancel_1',
'customer' => 'cus_test_1',
'cancel_at_period_end' => false,
],
'previous_attributes' => ['cancel_at_period_end' => true],
],
]));
expect(ActivityEvent::sole()->description)->toContain('Abo-Kündigung zurückgenommen');
});
test('a subscription update without cancellation transition records no activity', function () {
User::factory()->create(['stripe_id' => 'cus_test_1']);
app(ProcessStripeWebhook::class)->handle(new WebhookReceived([
'type' => 'customer.subscription.updated',
'data' => [
'object' => [
'id' => 'sub_cancel_1',
'customer' => 'cus_test_1',
'cancel_at_period_end' => false,
'items' => ['data' => [['price' => ['id' => 'price_x']]]],
],
'previous_attributes' => ['items' => []],
],
]));
expect(ActivityEvent::count())->toBe(0);
});
test('a fully ended subscription is recorded in the activity log', function () {
$user = User::factory()->create(['stripe_id' => 'cus_test_1']);
app(ProcessStripeWebhook::class)->handle(new WebhookReceived([
'type' => 'customer.subscription.deleted',
'data' => ['object' => ['id' => 'sub_cancel_1', 'customer' => 'cus_test_1']],
]));
$activityEvent = ActivityEvent::sole();
expect($activityEvent->description)->toContain('Abo beendet')
->and($activityEvent->user_id)->toBe($user->id);
});