Depublikation: Kauf als eingelöst markieren, Rechnung & Bestätigungs-Mail

Drei Lücken im bezahlten Depublikations-Flow (T12) geschlossen:

- Webhook setzt den Kauf jetzt auf status=consumed (vorher nur
  consumed_at) — erst damit erscheint er unter „Eingelöste Käufe".
- Einmalkäufe (Depublikation, Einzel-PM, Credit-Topup) aktivieren
  invoice_creation im Stripe-Checkout: Ohne das Flag erzeugt Stripe im
  Payment-Mode keine Rechnung und die STR-Spiegelung lief ins Leere.
  Abo-Checkouts bleiben unverändert (Parameter dort unzulässig).
- Neue Bestätigungs-Mail PressReleaseDepublished an den Käufer
  (nicht zwingend der PM-Autor) nach vollzogener Depublikation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-08 11:25:12 +00:00
parent d57f6b5c66
commit f6882b621a
7 changed files with 180 additions and 9 deletions

View file

@ -6,6 +6,7 @@ use App\Enums\InvoiceStatus;
use App\Enums\PressReleaseStatus;
use App\Enums\SinglePurchaseStatus;
use App\Enums\SinglePurchaseType;
use App\Mail\PressReleaseDepublished;
use App\Models\CreditTopup;
use App\Models\Invoice;
use App\Models\InvoiceBillingAddress;
@ -15,6 +16,7 @@ use App\Services\Billing\CreditTopupService;
use App\Services\Billing\InvoiceNumberGenerator;
use App\Services\PressRelease\PressReleaseService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Laravel\Cashier\Cashier;
use Laravel\Cashier\Events\WebhookReceived;
use Laravel\Cashier\Subscription;
@ -268,7 +270,18 @@ class ProcessStripeWebhook
}
app(PressReleaseService::class)->depublish($pressRelease);
$purchase->update(['consumed_at' => now()]);
$purchase->update([
'status' => SinglePurchaseStatus::Consumed->value,
'consumed_at' => now(),
]);
// Bestätigung an den Käufer (nicht zwingend der PM-Autor — der
// Kauf kann von einem Firmen-Kollegen mit Zugriff stammen).
$buyer = $purchase->user;
if ($buyer && $buyer->email) {
Mail::to($buyer->email)->queue(new PressReleaseDepublished($buyer, $pressRelease));
}
Log::info('Pressemitteilung depubliziert.', [
'single_purchase_id' => $purchase->id,

View file

@ -0,0 +1,44 @@
<?php
namespace App\Mail;
use App\Models\PressRelease;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
/**
* Bestätigung der kostenpflichtigen Depublikation (T12) an den Käufer:
* Die PM ist aus der öffentlichen Ansicht entfernt, unter der bisherigen
* URL bleibt nur der Hinweis auf die Entfernung (Tombstone) sichtbar.
*/
class PressReleaseDepublished extends Mailable
{
use Queueable, SerializesModels;
public string $showUrl;
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
) {
$this->showUrl = $user->canAccessAdmin()
? route('admin.press-releases.show', $pressRelease->id)
: route('me.press-releases.show', $pressRelease->id);
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Ihre Pressemitteilung wurde depubliziert: '.$this->pressRelease->title,
);
}
public function content(): Content
{
return new Content(view: 'emails.press-release.depublished');
}
}

View file

@ -341,7 +341,7 @@ class StripeCheckoutService
$this->syncCustomerFromBillingAddress($user);
return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [
...$this->sessionOptions($user),
...$this->oneTimeSessionOptions($user),
'metadata' => ['single_purchase_id' => (string) $purchase->id],
]);
}
@ -358,7 +358,7 @@ class StripeCheckoutService
return $user->checkout(
[$this->adHocLineItem($purchase->price_cents, 'Depublikation einer Pressemitteilung')],
[
...$this->sessionOptions($user),
...$this->oneTimeSessionOptions($user),
'metadata' => ['single_purchase_id' => (string) $purchase->id],
],
);
@ -376,9 +376,27 @@ class StripeCheckoutService
return $user->checkout(
[$this->adHocLineItem($topup->price_cents, "Credit-Guthaben ({$topup->credits} Credits)")],
[
...$this->sessionOptions($user),
...$this->oneTimeSessionOptions($user),
'metadata' => ['credit_topup_id' => (string) $topup->id],
],
);
}
/**
* Session-Optionen für Einmalkäufe (Payment-Mode: Einzel-PM, Depublikation,
* Credit-Topup). Anders als im Subscription-Mode erzeugt Stripe hier ohne
* `invoice_creation` keine Rechnung — und damit auch kein
* `invoice.payment_succeeded`, das die STR-Spiegelung anstoßen könnte.
* Im Subscription-Mode ist der Parameter unzulässig, daher nicht in
* sessionOptions() selbst.
*
* @return array<string, mixed>
*/
private function oneTimeSessionOptions(User $user): array
{
return [
...$this->sessionOptions($user),
'invoice_creation' => ['enabled' => true],
];
}
}