create(['is_active' => true]); $company = Company::factory()->businessportal24()->create(['name' => 'Muster GmbH']); $customer->companies()->attach($company->id, ['role' => 'member']); test()->actingAs($customer); return [$customer, $company]; } function viewsOn(PressRelease $release, int $count, int $daysAgo = 0): void { // created_at ist nicht fillable — wie im Produktivpfad setzt es das // Model; für die Zeitreihe wird es hier nachträglich verschoben. foreach (range(1, $count) as $i) { $event = PressReleaseEvent::query()->create(['press_release_id' => $release->id, 'type' => PressReleaseEventType::View, 'channel' => null]); $event->forceFill(['created_at' => now()->subDays($daysAgo)->setTime(10, $i % 60)])->saveQuietly(); } } test('the company summary counts every release of the company, whoever submitted it', function () { /** @var TestCase $this */ [, $company] = claimant(); $partner = User::factory()->create(); $old = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'user_id' => $partner->id, 'hits' => 120, 'published_at' => now()->subMonths(2)]); $new = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'user_id' => $partner->id, 'hits' => 30, 'published_at' => now()->subDays(3)]); PressRelease::factory()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'user_id' => $partner->id, 'status' => 'draft', 'hits' => 0]); viewsOn($old, 5, daysAgo: 40); viewsOn($old, 2, daysAgo: 2); viewsOn($new, 7, daysAgo: 1); $summary = app(VisibilityStats::class)->companySummary($company, 30); expect($summary['releases'])->toBe(3) ->and($summary['published'])->toBe(2) ->and($summary['views_total'])->toBe(150) ->and($summary['views_recent'])->toBe(9) ->and($summary['last_published_at']->isSameDay($new->published_at))->toBeTrue(); expect(app(VisibilityStats::class)->recentViewsFor([$old->id, $new->id], 30))->toBe([$old->id => 2, $new->id => 7]); }); test('the company page lists partner-submitted releases with their views', function () { /** @var TestCase $this */ // Vor 1c.3 filterte die Seite nach user_id — ein Beansprucher sah // „Keine Pressemitteilungen", obwohl connektar 68 eingereicht hatte. [, $company] = claimant(); $partner = User::factory()->create(); $release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create([ 'company_id' => $company->id, 'user_id' => $partner->id, 'title' => 'Partnermeldung mit Reichweite', 'hits' => 4321, 'published_at' => now()->subDays(2), ]); viewsOn($release, 3, daysAgo: 1); LivewireVolt::test('customer.press-kits.show', ['id' => $company->id]) ->assertSeeText('Partnermeldung mit Reichweite') ->assertSeeText('eingereicht über Partner') ->assertSeeText('4.321') ->assertSeeText('Aufrufe 30 Tage') ->assertDontSeeText('Keine Pressemitteilungen für diese Firma'); }); test('the daily series is gapless, marks days before measurement as unmeasured and respects retention', function () { /** @var TestCase $this */ config()->set('press_releases.events.recorded_since', now()->subDays(10)->toDateString()); [, $company] = claimant(); $release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'published_at' => now()->subDays(20)]); viewsOn($release, 4, daysAgo: 3); viewsOn($release, 1, daysAgo: 0); $series = app(VisibilityStats::class)->dailyViews($release, 14); expect($series)->toHaveCount(14) ->and($series[13]['views'])->toBe(1) ->and($series[10]['views'])->toBe(4) ->and($series[12]['views'])->toBe(0) // Tage vor Beginn der Aufzeichnung: keine Messung, nicht 0. ->and($series[0]['views'])->toBeNull() ->and($series[2]['views'])->toBeNull() // Der Starttag der Aufzeichnung selbst zählt schon. ->and($series[3]['views'])->toBe(0); config()->set('press_releases.events.retention_days', 5); expect(app(VisibilityStats::class)->dailyViews($release, 400))->toHaveCount(5); }); test('the release summary reports sitemap presence only for public releases and lists crawler fetches', function () { /** @var TestCase $this */ [, $company] = claimant(); $category = Category::factory()->create(['portal' => Portal::Businessportal24]); $category->translations()->create(['locale' => 'de', 'name' => 'Handel', 'slug' => 'handel']); $public = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'category_id' => $category->id, 'published_at' => now()->subDay(), 'hits' => 9]); $draft = PressRelease::factory()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'status' => 'draft']); PressReleaseEvent::query()->create(['press_release_id' => $public->id, 'type' => PressReleaseEventType::Share, 'channel' => 'linkedin']); viewsOn($public, 2, daysAgo: 1); PressReleaseCrawlerHit::factory()->service('google', 'Mozilla/5.0 (compatible; Googlebot/2.1)')->count(2)->create(['press_release_id' => $public->id]); PressReleaseCrawlerHit::factory()->count(1)->create(['press_release_id' => $public->id]); $stats = app(VisibilityStats::class); $summary = $stats->releaseSummary($public); expect($summary['hits'])->toBe(9) ->and($summary['shares_total'])->toBe(1) ->and($summary['in_sitemap_since']->isSameDay($public->published_at))->toBeTrue() ->and($summary)->not->toHaveKey('search_url') ->and(collect($summary['crawlers'])->pluck('fetches', 'service')->sortKeys()->all())->toBe(['google' => 2, 'openai' => 1]) ->and(collect($summary['crawlers'])->firstWhere('service', 'google')['label'])->toBe('Google Suche') ->and($summary['first_view_at'])->not->toBeNull() // RSS-Feeds (3.1): Branchen-Feed und Portal-Feed des eigenen Portals. ->and(collect($summary['feeds'])->pluck('url')->all())->toBe([ 'https://businessportal24.test/de/category/handel/feed', 'https://businessportal24.test/de/newsroom/'.$company->slug.'/feed', 'https://businessportal24.test/de/feed', ]); $draftSummary = $stats->releaseSummary($draft); expect($draftSummary['in_sitemap_since'])->toBeNull() ->and($draftSummary['crawlers'])->toBe([]) ->and($draftSummary['feeds'])->toBe([]); }); test('the release page shows the delivery panel with daily bars, sitemap date and crawler fetches, but no search link', function () { /** @var TestCase $this */ [$customer, $company] = claimant(); $partner = User::factory()->create(); $release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create([ 'company_id' => $company->id, 'user_id' => $partner->id, 'hits' => 777, 'published_at' => now()->subDays(5), ]); viewsOn($release, 6, daysAgo: 2); PressReleaseCrawlerHit::factory()->service('anthropic', 'ClaudeBot/1.0')->create(['press_release_id' => $release->id]); LivewireVolt::test('customer.press-releases.show', ['id' => $release->id]) ->assertSeeText('Auslieferung') ->assertSeeText('777') ->assertSeeText('Aufrufe 30 Tage') ->assertDontSeeText('Bei Google prüfen') ->assertDontSee('google.com/search', false) ->assertSeeText('Gelistet seit '.now()->subDays(5)->format('d.m.Y')) ->assertSeeText('Anthropic / Claude') ->assertSeeText('1 Abruf') ->assertSeeText('Ein Abruf belegt, dass der Crawler des Dienstes die Seite geholt hat') ->assertSeeText('RSS-Feeds') ->assertSee('https://businessportal24.test/de/feed', false) // Indexierungsstatus bleibt zu (E8), solange die Kundenansicht nicht freigegeben ist. ->assertDontSeeText('Google-Index') ->assertSee('set('viewRange', 90) ->assertSeeText('Aufrufe 90 Tage'); }); test('the index status appears in the delivery panel only once the customer view is released', function () { /** @var TestCase $this */ [, $company] = claimant(); $release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'published_at' => now()->subDays(5)]); PressReleaseIndexCheck::factory()->withState(IndexState::CrawledNotIndexed)->create(['press_release_id' => $release->id, 'portal' => Portal::Businessportal24, 'checked_at' => now()->subDay()]); PressReleaseIndexCheck::factory()->withState(IndexState::Indexed)->create(['press_release_id' => $release->id, 'portal' => Portal::Businessportal24, 'checked_at' => now()->subDays(9)]); config()->set('search_console.customer_visible', false); LivewireVolt::test('customer.press-releases.show', ['id' => $release->id]) ->assertDontSeeText('Google-Index'); config()->set('search_console.customer_visible', true); LivewireVolt::test('customer.press-releases.show', ['id' => $release->id]) ->assertSeeText('Google-Index') ->assertSeeText('Businessportal24: gecrawlt, nicht indexiert') ->assertDontSeeText('Businessportal24: indexiert'); }); test('a draft shows no visibility panel', function () { /** @var TestCase $this */ [$customer, $company] = claimant(); $draft = PressRelease::factory()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'user_id' => $customer->id, 'status' => 'draft', 'hits' => 0, 'published_at' => null]); LivewireVolt::test('customer.press-releases.show', ['id' => $draft->id]) ->assertDontSeeText('Auslieferung') ->assertSeeText('noch nicht veröffentlicht'); }); test('the release summary reports the sitemap role: the source is listed, a copy is not', function () { /** @var TestCase $this */ [, $company] = claimant(); config()->set('canonical.enabled', true); config()->set('canonical.portal_by_category', ['handel' => 'presseecho']); $category = Category::factory()->create(['portal' => Portal::Both]); $category->translations()->create(['locale' => 'de', 'name' => 'Handel', 'slug' => 'handel']); $source = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'category_id' => $category->id, 'published_at' => now()->subDay()]); $copy = PressRelease::factory()->published()->forPortal(Portal::Presseecho)->create(['company_id' => $company->id, 'category_id' => $category->id, 'published_at' => now()->subDay(), 'canonical_press_release_id' => $source->id]); $both = PressRelease::factory()->published()->forPortal(Portal::Both)->create(['company_id' => $company->id, 'category_id' => $category->id, 'published_at' => now()->subDay()]); $stats = app(VisibilityStats::class); $sourceSummary = $stats->releaseSummary($source); $copySummary = $stats->releaseSummary($copy); $bothSummary = $stats->releaseSummary($both); expect($sourceSummary['sitemap_role'])->toBe('source') ->and($sourceSummary['sitemap_portals'])->toBe([Portal::Businessportal24->label()]) ->and($sourceSummary['in_sitemap_since'])->not->toBeNull() // Die Kopie ist öffentlich, steht aber in keiner Sitemap (SitemapController, 1c.8). ->and($copySummary['sitemap_role'])->toBe('copy') ->and($copySummary['sitemap_portals'])->toBe([]) ->and($copySummary['in_sitemap_since'])->toBeNull() // portal=both: nur die Sitemap des Kategorie-Portals führt sie. ->and($bothSummary['sitemap_role'])->toBe('source') ->and($bothSummary['sitemap_portals'])->toBe([Portal::Presseecho->label()]); // Ohne Zuordnung ist jede Fassung Erstquelle. config()->set('canonical.enabled', false); expect($stats->releaseSummary($copy)['sitemap_role'])->toBe('source'); }); test('the listing date is not claimed when the release has a counterpart on the other portal', function () { /** @var TestCase $this */ [$customer, $company] = claimant(); config()->set('canonical.enabled', true); // Fassung auf dem anderen Portal, inzwischen depubliziert: Die Kopie ist // erst seitdem Erstquelle — wann genau, ist nicht gespeichert (N17-Rest). $former = PressRelease::factory()->forPortal(Portal::Presseecho)->create(['company_id' => $company->id, 'title' => 'Gleich', 'text' => 'Gleicher Text', 'status' => 'archived', 'published_at' => now()->subDays(20)]); $release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'user_id' => $customer->id, 'title' => 'Gleich', 'text' => 'Gleicher Text', 'published_at' => now()->subDays(10)]); expect($former->content_hash)->toBe($release->content_hash); $summary = app(VisibilityStats::class)->releaseSummary($release); expect($summary['sitemap_role'])->toBe('source') ->and($summary['in_sitemap_since'])->toBeNull() ->and($summary['sitemap_portals'])->toBe([Portal::Businessportal24->label()]); LivewireVolt::test('customer.press-releases.show', ['id' => $release->id]) ->assertSeeText('In der Sitemap gelistet (Businessportal24)') ->assertDontSeeText('Gelistet seit') ->assertDontSeeText('Derzeit nicht gelistet'); });