From ed458b68d2519aa72842deef74c0f53526b8c6ab Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 21 Jul 2026 13:11:38 +0000 Subject: [PATCH] admin: Verlauf protokolliert den Gang zur Stripe-Zahlung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer ActivityType "Zur Zahlung": Der StripeCheckoutService vermerkt zentral in allen vier Checkout-Erzeugern (Abo, Einzel-PM, Depublikation, Credit-Topup), wenn ein User zu Stripe weitergeleitet wird — mit Art und Referenz in den Metadaten. Der Abschluss war bereits abgedeckt (Webhook: "Zahlung eingegangen" mit Rechnungsnummer/Betrag, Abo-Events als "Abo", Wallet-Aufladungen als "Credit-Aufladung"). Co-Authored-By: Claude Fable 5 --- app/Enums/ActivityType.php | 3 + .../Billing/StripeCheckoutService.php | 58 +++++++++++++++++-- tests/Feature/ActivityLogTest.php | 15 +++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/app/Enums/ActivityType.php b/app/Enums/ActivityType.php index 6eb49f87..10db2a37 100644 --- a/app/Enums/ActivityType.php +++ b/app/Enums/ActivityType.php @@ -23,6 +23,7 @@ enum ActivityType: string case MailSent = 'mail_sent'; case EmailUnsubscribe = 'email_unsubscribe'; case AdminGrant = 'admin_grant'; + case CheckoutStarted = 'checkout_started'; public function label(): string { @@ -41,6 +42,7 @@ enum ActivityType: string self::MailSent => 'E-Mail', self::EmailUnsubscribe => 'Mail-Abmeldung', self::AdminGrant => 'Gutschrift', + self::CheckoutStarted => 'Zur Zahlung', }; } @@ -59,6 +61,7 @@ enum ActivityType: string self::MailSent => 'purple', self::EmailUnsubscribe => 'orange', self::AdminGrant => 'amber', + self::CheckoutStarted => 'yellow', }; } } diff --git a/app/Services/Billing/StripeCheckoutService.php b/app/Services/Billing/StripeCheckoutService.php index 4956aefa..bfbe58db 100644 --- a/app/Services/Billing/StripeCheckoutService.php +++ b/app/Services/Billing/StripeCheckoutService.php @@ -2,10 +2,12 @@ namespace App\Services\Billing; +use App\Enums\ActivityType; use App\Models\CreditTopup; use App\Models\Plan; use App\Models\SinglePurchase; use App\Models\User; +use App\Services\ActivityLog; use Carbon\CarbonInterface; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; @@ -28,8 +30,27 @@ class StripeCheckoutService public function __construct( private readonly VatResolver $vatResolver, private readonly VatIdValidationService $vatIdValidation, + private readonly ActivityLog $activityLog, ) {} + /** + * Verlaufs-Eintrag „zur Zahlung gegangen": wird von allen + * Checkout-Erzeugern aufgerufen, sobald die Stripe-Session steht und + * der User zu Stripe weitergeleitet wird. Der Abschluss (oder Abbruch) + * folgt separat über den Webhook („Zahlung"/„Abo"). + * + * @param array $meta + */ + public function recordCheckoutStarted(User $user, string $what, array $meta = []): void + { + $this->activityLog->record( + ActivityType::CheckoutStarted, + sprintf('%s (%s) ist zur Zahlung weitergeleitet — %s.', $user->name, $user->email, $what), + user: $user, + meta: $meta, + ); + } + /** * Stripe-Checkout für ein Tarif-Abo (monatlich/jährlich). Die Steuer * ergänzt Stripe Tax automatisch (Cashier::calculateTaxes, Netto-Preise). @@ -42,9 +63,17 @@ class StripeCheckoutService $this->syncCustomerFromBillingAddress($user); - return $user + $checkout = $user ->newSubscription('default', $priceId) ->checkout($this->sessionOptions($user)); + + $this->recordCheckoutStarted($user, sprintf( + 'Abo „%s" (%s)', + $plan->name, + $interval === 'yearly' ? 'jährlich' : 'monatlich', + ), ['kind' => 'subscription', 'plan_id' => $plan->id, 'interval' => $interval]); + + return $checkout; } /** @@ -402,10 +431,17 @@ class StripeCheckoutService { $this->syncCustomerFromBillingAddress($user); - return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [ + $checkout = $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [ ...$this->oneTimeSessionOptions($user), 'metadata' => ['single_purchase_id' => (string) $purchase->id], ]); + + $this->recordCheckoutStarted($user, 'Einzel-Pressemitteilung', [ + 'kind' => 'single_pm', + 'single_purchase_id' => $purchase->id, + ]); + + return $checkout; } /** @@ -417,13 +453,20 @@ class StripeCheckoutService { $this->syncCustomerFromBillingAddress($user); - return $user->checkout( + $checkout = $user->checkout( [$this->adHocLineItem($purchase->price_cents, 'Depublikation einer Pressemitteilung')], [ ...$this->oneTimeSessionOptions($user), 'metadata' => ['single_purchase_id' => (string) $purchase->id], ], ); + + $this->recordCheckoutStarted($user, 'Depublikation einer Pressemitteilung', [ + 'kind' => 'depublication', + 'single_purchase_id' => $purchase->id, + ]); + + return $checkout; } /** @@ -435,13 +478,20 @@ class StripeCheckoutService { $this->syncCustomerFromBillingAddress($user); - return $user->checkout( + $checkout = $user->checkout( [$this->adHocLineItem($topup->price_cents, "Credit-Guthaben ({$topup->credits} Credits)")], [ ...$this->oneTimeSessionOptions($user), 'metadata' => ['credit_topup_id' => (string) $topup->id], ], ); + + $this->recordCheckoutStarted($user, "Credit-Paket ({$topup->credits} Credits)", [ + 'kind' => 'credit_topup', + 'credit_topup_id' => $topup->id, + ]); + + return $checkout; } /** diff --git a/tests/Feature/ActivityLogTest.php b/tests/Feature/ActivityLogTest.php index bea2e281..311b96d8 100644 --- a/tests/Feature/ActivityLogTest.php +++ b/tests/Feature/ActivityLogTest.php @@ -8,6 +8,7 @@ use App\Models\ContactMessage; use App\Models\PressRelease; use App\Models\User; use App\Models\WithdrawalRequest; +use App\Services\Billing\StripeCheckoutService; use App\Services\PressRelease\PressReleaseService; use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Auth\Events\Login; @@ -139,3 +140,17 @@ test('a mail to an unknown address is recorded with the address as actor label', expect($event->user_id)->toBeNull() ->and($event->actor_label)->toBe('extern@example.com'); }); + +test('a checkout redirect is recorded in the activity log', function () { + $user = User::factory()->create(['name' => 'Kauf Karla']); + + app(StripeCheckoutService::class) + ->recordCheckoutStarted($user, 'Abo „Business" (monatlich)', ['kind' => 'subscription', 'interval' => 'monthly']); + + $event = ActivityEvent::query()->where('type', ActivityType::CheckoutStarted->value)->sole(); + + expect($event->user_id)->toBe($user->id) + ->and($event->description)->toContain('zur Zahlung weitergeleitet') + ->and($event->description)->toContain('Abo „Business" (monatlich)') + ->and($event->meta['kind'])->toBe('subscription'); +});