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:
parent
d57f6b5c66
commit
f6882b621a
7 changed files with 180 additions and 9 deletions
|
|
@ -5,6 +5,7 @@ use App\Enums\PressReleaseStatus;
|
|||
use App\Enums\SinglePurchaseStatus;
|
||||
use App\Enums\SinglePurchaseType;
|
||||
use App\Listeners\ProcessStripeWebhook;
|
||||
use App\Mail\PressReleaseDepublished;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\BookingConfirmation;
|
||||
use App\Models\PressRelease;
|
||||
|
|
@ -13,6 +14,7 @@ use App\Models\User;
|
|||
use App\Services\Billing\StripeCheckoutService;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Laravel\Cashier\Checkout;
|
||||
use Laravel\Cashier\Events\WebhookReceived;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
|
|
@ -193,6 +195,7 @@ test('the detail page hides the depublication offer without a price and after a
|
|||
|
||||
test('the paid webhook depublishes the release and the public page shows a tombstone', function () {
|
||||
/** @var TestCase $this */
|
||||
Mail::fake();
|
||||
$user = depublicationCustomer();
|
||||
$release = publishedReleaseFor($user);
|
||||
|
||||
|
|
@ -220,10 +223,18 @@ test('the paid webhook depublishes the release and the public page shows a tombs
|
|||
$release->refresh();
|
||||
expect($release->status)->toBe(PressReleaseStatus::Archived);
|
||||
expect($release->depublished_at)->not->toBeNull();
|
||||
expect($purchase->fresh()->status)->toBe(SinglePurchaseStatus::Paid);
|
||||
// Eingelöst = Status Consumed — nur so erscheint der Kauf unter
|
||||
// „Eingelöste Käufe" auf der Buchungs-Seite.
|
||||
expect($purchase->fresh()->status)->toBe(SinglePurchaseStatus::Consumed);
|
||||
expect($purchase->fresh()->consumed_at)->not->toBeNull();
|
||||
expect($release->statusLogs()->count())->toBeGreaterThan(0);
|
||||
|
||||
// Der Käufer erhält genau eine Bestätigung (Replay löst keine zweite aus).
|
||||
Mail::assertQueued(PressReleaseDepublished::class, function (PressReleaseDepublished $mail) use ($user, $release) {
|
||||
return $mail->hasTo($user->email) && $mail->pressRelease->is($release);
|
||||
});
|
||||
Mail::assertQueuedCount(1);
|
||||
|
||||
// Öffentliche URL: Tombstone mit 410 Gone statt 404, ohne Inhalte.
|
||||
$this->get('https://businessportal24.test/de/press-release/'.$release->slug)
|
||||
->assertStatus(410)
|
||||
|
|
|
|||
|
|
@ -71,13 +71,29 @@ test('checkout session options collect the address in stripe when no billing add
|
|||
->and($options['customer_update'])->toBe(['address' => 'auto', 'name' => 'auto']);
|
||||
});
|
||||
|
||||
test('one-time checkouts enable stripe invoice creation, subscription checkouts do not', function () {
|
||||
$user = User::factory()->create();
|
||||
BillingAddress::factory()->create(['user_id' => $user->id]);
|
||||
$user = $user->fresh();
|
||||
|
||||
// Payment-Mode: ohne invoice_creation erzeugt Stripe keine Rechnung —
|
||||
// und damit gäbe es kein invoice.payment_succeeded für die STR-Spiegelung.
|
||||
$oneTime = invokeSessionOptions($user, 'oneTimeSessionOptions');
|
||||
expect($oneTime['invoice_creation'])->toBe(['enabled' => true])
|
||||
->and($oneTime['automatic_tax'])->toBe(['enabled' => true]);
|
||||
|
||||
// Subscription-Mode lehnt invoice_creation ab — dort erzeugt Stripe
|
||||
// die Rechnungen ohnehin selbst.
|
||||
expect(invokeSessionOptions($user))->not->toHaveKey('invoice_creation');
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function invokeSessionOptions(User $user): array
|
||||
function invokeSessionOptions(User $user, string $method = 'sessionOptions'): array
|
||||
{
|
||||
$service = app(StripeCheckoutService::class);
|
||||
$method = new ReflectionMethod($service, 'sessionOptions');
|
||||
$method = new ReflectionMethod($service, $method);
|
||||
|
||||
return $method->invoke($service, $user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Mail\ContactRequestSubmitted;
|
||||
use App\Mail\GoLivePasswordReset;
|
||||
use App\Mail\MagicLoginLink;
|
||||
use App\Mail\PressReleaseDepublished;
|
||||
use App\Mail\PressReleasePublished;
|
||||
use App\Mail\PressReleaseRejected;
|
||||
use App\Mail\TerminationRequestReceived;
|
||||
|
|
@ -20,7 +22,9 @@ use Illuminate\Auth\Notifications\VerifyEmail;
|
|||
* Alle System-Mails müssen im pressekonto-CI erscheinen: Wortmarke,
|
||||
* „Publisher · Hub"-Tagline und die Zugehörigkeits-Erklärung zur
|
||||
* Brand-Familie (presseecho + businessportal24) aus dem zentralen
|
||||
* E-Mail-Layout (components/emails/layout).
|
||||
* E-Mail-Layout (components/emails/layout). Zusätzlich: Hub verlinkt,
|
||||
* Rechts-Links (AGB/Impressum/Datenschutz) im Footer und keine direkte
|
||||
* Service-Mailadresse im Fließtext.
|
||||
*/
|
||||
function assertHubEmailBranding(string $html): void
|
||||
{
|
||||
|
|
@ -30,7 +34,15 @@ function assertHubEmailBranding(string $html): void
|
|||
->toContain('businessportal24')
|
||||
->toContain('ist der gemeinsame Publisher')
|
||||
->toContain('#1A2540')
|
||||
->toContain('#B07A3A');
|
||||
->toContain('#B07A3A')
|
||||
// Hub ist verlinkt (Wortmarke im Header + Brand-Familien-Zeile).
|
||||
->toContain('href="'.config('domains.domain_portal_url').'"')
|
||||
// Rechtliche Links im Footer (alle auf dem Hub).
|
||||
->toContain(config('domains.domain_portal_url').'/de/agb')
|
||||
->toContain(config('domains.domain_portal_url').'/de/impressum')
|
||||
->toContain(config('domains.domain_portal_url').'/de/datenschutz')
|
||||
// Keine direkte Service-Mailadresse in der Mail.
|
||||
->not->toContain(config('mail.from.address'));
|
||||
}
|
||||
|
||||
test('the magic login link mail uses the hub ci layout', function () {
|
||||
|
|
@ -53,6 +65,42 @@ test('the press release published and rejected mails use the hub ci layout', fun
|
|||
assertHubEmailBranding((new PressReleaseRejected($user, $pressRelease, 'Bitte Quellen ergänzen.'))->render());
|
||||
});
|
||||
|
||||
test('the press release depublished mail uses the hub ci layout and explains the tombstone', function () {
|
||||
$pressRelease = PressRelease::factory()->published()->create(['depublished_at' => now()]);
|
||||
$user = $pressRelease->user;
|
||||
|
||||
$html = (new PressReleaseDepublished($user, $pressRelease))->render();
|
||||
|
||||
assertHubEmailBranding($html);
|
||||
expect($html)
|
||||
->toContain($pressRelease->title)
|
||||
->toContain('Depubliziert am')
|
||||
->toContain('auf Wunsch des Absenders entfernt');
|
||||
});
|
||||
|
||||
test('the published mail names the portal with a link to the public detail page', function () {
|
||||
$pressRelease = PressRelease::factory()->published()->forPortal(Portal::Presseecho)->create();
|
||||
|
||||
$html = (new PressReleasePublished($pressRelease->user, $pressRelease))->render();
|
||||
|
||||
expect($html)
|
||||
->toContain('Veröffentlicht auf')
|
||||
->toContain('Presseecho')
|
||||
->toContain(config('domains.domain_presseecho_url').'/de/press-release/'.$pressRelease->slug);
|
||||
});
|
||||
|
||||
test('the published mail links both portals for a cross-portal press release', function () {
|
||||
$pressRelease = PressRelease::factory()->published()->forPortal(Portal::Both)->create();
|
||||
|
||||
$html = (new PressReleasePublished($pressRelease->user, $pressRelease))->render();
|
||||
|
||||
expect($html)
|
||||
->toContain('Presseecho')
|
||||
->toContain('Businessportal24')
|
||||
->toContain(config('domains.domain_presseecho_url').'/de/press-release/'.$pressRelease->slug)
|
||||
->toContain(config('domains.domain_businessportal_url').'/de/press-release/'.$pressRelease->slug);
|
||||
});
|
||||
|
||||
test('the termination mails use the hub ci layout', function () {
|
||||
$terminationRequest = TerminationRequest::factory()->create();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue