diff --git a/app/Services/PressRelease/BoostService.php b/app/Services/PressRelease/BoostService.php index a2d4d4f8..4bb96bd9 100644 --- a/app/Services/PressRelease/BoostService.php +++ b/app/Services/PressRelease/BoostService.php @@ -2,6 +2,7 @@ namespace App\Services\PressRelease; +use App\Enums\Portal; use App\Enums\PressReleaseClassification; use App\Enums\PressReleaseStatus; use App\Exceptions\BoostNotAllowedException; @@ -11,6 +12,7 @@ use App\Models\User; use App\Services\Billing\CreditPricingService; use App\Services\Billing\CreditWalletService; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; /** @@ -63,7 +65,7 @@ class BoostService $credits = $this->pricing->boostCredits($days); - return DB::transaction(function () use ($user, $pressRelease, $days, $credits): Boost { + $boost = DB::transaction(function () use ($user, $pressRelease, $days, $credits): Boost { $startsAt = $this->activeUntil($pressRelease) ?? now(); $boost = $pressRelease->boosts()->create([ @@ -83,5 +85,39 @@ class BoostService return $boost; }); + + $this->forgetPlacementCaches($pressRelease); + + return $boost; + } + + /** + * Gecachte Start-/Branchenseiten der betroffenen Ausgaben verwerfen, + * damit die bezahlte Platzierung sofort sichtbar wird statt erst nach + * Ablauf der Cache-TTL (HomePageData/CategoryPageData, 300 s). + */ + private function forgetPlacementCaches(PressRelease $pressRelease): void + { + $portals = $pressRelease->portal === Portal::Both + ? [Portal::Presseecho->value, Portal::Businessportal24->value] + : [$pressRelease->portal->value]; + + // Branchenseite der eigenen Rubrik plus Eltern-Rubrik (deren Seite + // schließt Unterkategorien mit ein), je hinterlegter Sprache. + $category = $pressRelease->category() + ->with(['translations', 'parent.translations']) + ->first(); + $translations = collect($category?->translations ?? []) + ->merge($category?->parent?->translations ?? []); + + foreach ($portals as $portal) { + Cache::forget(sprintf('web.home.%s.%s', $portal, $pressRelease->language)); + + foreach ($translations as $translation) { + if (filled($translation->slug)) { + Cache::forget(sprintf('web.category.%s.%s.%s', $portal, $translation->locale, $translation->slug)); + } + } + } } } diff --git a/tests/Feature/Web/BoostPlacementTest.php b/tests/Feature/Web/BoostPlacementTest.php index 3f9162d5..90087741 100644 --- a/tests/Feature/Web/BoostPlacementTest.php +++ b/tests/Feature/Web/BoostPlacementTest.php @@ -1,10 +1,14 @@ 'Geboostete aeltere Meldung', 'slug' => 'geboostete-aeltere-meldung', 'published_at' => now()->subDays(5), + // Grün klassifiziert: Voraussetzung fürs Boost-Gate im BoostService. + 'classification' => PressReleaseClassification::Green->value, ]); return compact('category', 'company', 'newest', 'boosted'); @@ -82,6 +88,28 @@ test('an expired boost does not influence the homepage placement', function () { ->assertDontSeeText('Top-Platzierung'); }); +test('booking a boost invalidates the cached start and category pages immediately', function () { + /** @var TestCase $this */ + ['boosted' => $boosted, 'newest' => $newest] = boostPlacementFixture(); + + $owner = User::factory()->create(['is_active' => true]); + app(CreditWalletService::class)->credit($owner, 40); + + // Beide Seiten in den Cache legen — noch ohne Boost. + expect(app(HomePageData::class)->build(Portal::Presseecho)['leadRelease']->id)->toBe($newest->id); + $this->get('https://presseecho.test/de/category/energie-klima') + ->assertSuccessful() + ->assertDontSeeText('Top-Platzierung'); + + app(BoostService::class)->boost($owner, $boosted, 7); + + // Kein Warten auf die TTL: die Buchung verwirft die gecachten Seiten. + expect(app(HomePageData::class)->build(Portal::Presseecho)['leadRelease']->id)->toBe($boosted->id); + $this->get('https://presseecho.test/de/category/energie-klima') + ->assertSuccessful() + ->assertSeeText('Top-Platzierung'); +}); + test('a boosted release leads its category page and is labelled', function () { /** @var TestCase $this */ ['boosted' => $boosted] = boostPlacementFixture();