Release v1.4.0 #146

Merged
myrmidex merged 71 commits from release/v1.4.0 into main 2026-08-15 00:36:54 +02:00
7 changed files with 458 additions and 156 deletions
Showing only changes of commit a6eff35586 - Show all commits

View file

@ -2,10 +2,11 @@
namespace App\Http\Controllers\Api\V1;
use App\Models\Article;
use App\Services\DashboardStatsService;
use App\Support\DateRange;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class DashboardController extends BaseController
{
@ -18,23 +19,26 @@ public function __construct(
*/
public function stats(Request $request): JsonResponse
{
$period = $request->get('period', 'today');
$validated = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date', 'after_or_equal:from'],
]);
$range = isset($validated['from'], $validated['to'])
? new DateRange(
Carbon::parse($validated['from'])->startOfDay(),
Carbon::parse($validated['to'])->endOfDay(),
)
: DateRange::preset('today');
try {
// Get article stats from service
$articleStats = $this->dashboardStatsService->getStats($period);
// Get system stats
$systemStats = $this->dashboardStatsService->getSystemStats();
// Get available periods
$availablePeriods = $this->dashboardStatsService->getAvailablePeriods();
return $this->sendResponse([
'article_stats' => $articleStats,
'system_stats' => $systemStats,
'available_periods' => $availablePeriods,
'current_period' => $period,
'article_stats' => $this->dashboardStatsService->getStats($range),
'system_stats' => $this->dashboardStatsService->getSystemStats(),
'range' => [
'from' => $range->from->toDateString(),
'to' => $range->to->toDateString(),
],
]);
} catch (\Exception $e) {
return $this->sendError('Failed to fetch dashboard stats: '.$e->getMessage(), [], 500);

View file

@ -3,35 +3,132 @@
namespace App\Livewire;
use App\Services\DashboardStatsService;
use App\Support\DateRange;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Carbon;
use InvalidArgumentException;
use Livewire\Attributes\Computed;
use Livewire\Component;
class Dashboard extends Component
{
public string $period = 'today';
public string $from = '';
public string $to = '';
public function mount(): void
{
// Default period
$this->applyPreset('today');
}
public function setPeriod(string $period): void
public function applyPreset(string $preset): void
{
$this->period = $period;
try {
$range = DateRange::preset($preset);
} catch (InvalidArgumentException) {
return;
}
$this->from = $range->from->toDateString();
$this->to = $range->to->toDateString();
}
/**
* @return array<string, string>
*/
public function presets(): array
{
return DateRange::presets();
}
public bool $rangeIsValid = true;
public function range(): ?DateRange
{
$from = $this->parseBoundary($this->from);
$to = $this->parseBoundary($this->to);
if (! $from instanceof Carbon || ! $to instanceof Carbon) {
return null;
}
try {
return new DateRange($from->startOfDay(), $to->endOfDay());
} catch (InvalidArgumentException) {
return null;
}
}
private function parseBoundary(string $value): ?Carbon
{
try {
return Carbon::parse($value);
} catch (\Exception) {
return null;
}
}
/**
* @return array<string, mixed>
*/
#[Computed]
public function articleStats(): array
{
$range = $this->range();
if (! $range instanceof DateRange) {
return [
'articles_fetched' => 0,
'articles_published' => 0,
'published_percentage' => 0.0,
];
}
return app(DashboardStatsService::class)->getStats($range);
}
/**
* @return array<string, int>
*/
#[Computed]
public function systemStats(): array
{
return app(DashboardStatsService::class)->getSystemStats();
}
public function updated(string $property): void
{
if (in_array($property, ['from', 'to'], true)) {
$this->validateRange();
}
}
private function validateRange(): void
{
$this->resetErrorBag(['from', 'to']);
$from = $this->parseBoundary($this->from);
$to = $this->parseBoundary($this->to);
if (! $from instanceof Carbon) {
$this->addError('from', 'The start date is not a valid date.');
}
if (! $to instanceof Carbon) {
$this->addError('to', 'The end date is not a valid date.');
}
if ($from instanceof Carbon && $to instanceof Carbon && $to->lessThan($from)) {
$this->addError('to', 'The end date must not be earlier than the start date.');
}
$this->rangeIsValid = $this->range() instanceof DateRange;
}
public function render(): View
{
$service = app(DashboardStatsService::class);
$articleStats = $service->getStats($this->period);
$systemStats = $service->getSystemStats();
$availablePeriods = $service->getAvailablePeriods();
return view('livewire.dashboard', [
'articleStats' => $articleStats,
'systemStats' => $systemStats,
'availablePeriods' => $availablePeriods,
'presets' => $this->presets(),
])->layout('layouts.app');
}
}

View file

@ -8,33 +8,25 @@
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\Route;
use Carbon\Carbon;
use App\Support\DateRange;
class DashboardStatsService
{
/**
* @return array<string, mixed>
*/
public function getStats(string $period = 'today'): array
public function getStats(DateRange $range): array
{
$dateRange = $this->getDateRange($period);
$bounds = [$range->from, $range->to];
// Get articles fetched for the period
$articlesFetchedQuery = Article::query();
if ($dateRange) {
$articlesFetchedQuery->whereBetween('created_at', $dateRange);
}
$articlesFetched = $articlesFetchedQuery->count();
$articlesFetched = Article::query()
->whereBetween('created_at', $bounds)
->count();
// Get articles published for the period
$articlesPublishedQuery = ArticlePublication::query()
->whereNotNull('published_at');
if ($dateRange) {
$articlesPublishedQuery->whereBetween('published_at', $dateRange);
}
$articlesPublished = $articlesPublishedQuery->count();
$articlesPublished = ArticlePublication::query()
->whereBetween('published_at', $bounds)
->count();
// Calculate published percentage
$publishedPercentage = $articlesFetched > 0 ? round(($articlesPublished / $articlesFetched) * 100, 1) : 0.0;
return [
@ -44,37 +36,6 @@ public function getStats(string $period = 'today'): array
];
}
/**
* @return array<string, string>
*/
public function getAvailablePeriods(): array
{
return [
'today' => 'Today',
'week' => 'This Week',
'month' => 'This Month',
'year' => 'This Year',
'all' => 'All Time',
];
}
/**
* @return array{0: Carbon, 1: Carbon}|null
*/
private function getDateRange(string $period): ?array
{
$now = Carbon::now();
return match ($period) {
'today' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
'week' => [$now->copy()->startOfWeek(), $now->copy()->endOfWeek()],
'month' => [$now->copy()->startOfMonth(), $now->copy()->endOfMonth()],
'year' => [$now->copy()->startOfYear(), $now->copy()->endOfYear()],
'all' => null, // No date filtering for all-time stats
default => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
};
}
/**
* @return array<string, int>
*/

View file

@ -16,8 +16,8 @@
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Feeds</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $systemStats['active_feeds'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $systemStats['total_feeds'] }}</span>
{{ $this->systemStats['active_feeds'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $this->systemStats['total_feeds'] }}</span>
</p>
</div>
</div>
@ -34,8 +34,8 @@
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Platform Accounts</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $systemStats['active_platform_accounts'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $systemStats['total_platform_accounts'] }}</span>
{{ $this->systemStats['active_platform_accounts'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $this->systemStats['total_platform_accounts'] }}</span>
</p>
</div>
</div>
@ -52,8 +52,8 @@
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Platform Channels</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $systemStats['active_platform_channels'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $systemStats['total_platform_channels'] }}</span>
{{ $this->systemStats['active_platform_channels'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $this->systemStats['total_platform_channels'] }}</span>
</p>
</div>
</div>
@ -70,8 +70,8 @@
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Routes</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $systemStats['active_routes'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $systemStats['total_routes'] }}</span>
{{ $this->systemStats['active_routes'] }}
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">/{{ $this->systemStats['total_routes'] }}</span>
</p>
</div>
</div>
@ -81,18 +81,51 @@
<!-- Article Statistics -->
<div>
<div class="flex items-center justify-between mb-4">
<div class="flex flex-col gap-3 mb-4 md:flex-row md:items-end md:justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Article Statistics</h2>
<select
wire:model.live="period"
class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600"
>
@foreach ($availablePeriods as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
<div class="flex flex-col gap-2 sm:flex-row sm:items-end">
<div class="flex flex-wrap gap-1">
@foreach ($presets as $value => $label)
<button
type="button"
wire:click="applyPreset('{{ $value }}')"
class="rounded-md border border-gray-300 px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
>{{ $label }}</button>
@endforeach
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="flex items-end gap-2">
<div>
<label for="dashboard-from" class="block text-xs font-medium text-gray-500 dark:text-gray-400">From</label>
<input
id="dashboard-from"
type="date"
wire:model.live="from"
class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
<div>
<label for="dashboard-to" class="block text-xs font-medium text-gray-500 dark:text-gray-400">To</label>
<input
id="dashboard-to"
type="date"
wire:model.live="to"
class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600"
/>
</div>
</div>
</div>
</div>
@error('from')
<p class="mb-4 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
@error('to')
<p class="mb-4 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
<div @class(['grid grid-cols-1 md:grid-cols-3 gap-6', 'opacity-40' => ! $rangeIsValid])>
<!-- Articles Fetched -->
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
<div class="flex items-center">
@ -104,7 +137,7 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Articles Fetched</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $articleStats['articles_fetched'] }}
{{ $this->articleStats['articles_fetched'] }}
</p>
</div>
</div>
@ -121,7 +154,7 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Articles Published</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $articleStats['articles_published'] }}
{{ $this->articleStats['articles_published'] }}
</p>
</div>
</div>
@ -138,7 +171,7 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
<div class="ml-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Published Rate</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{{ $articleStats['published_percentage'] }}%
{{ $this->articleStats['published_percentage'] }}%
</p>
</div>
</div>

View file

@ -8,6 +8,7 @@
use App\Models\PlatformChannel;
use App\Models\Route;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;
class DashboardControllerTest extends TestCase
@ -35,72 +36,103 @@ public function test_stats_returns_successful_response(): void
'total_routes',
'active_routes',
],
'available_periods',
'current_period',
'range' => ['from', 'to'],
],
'message',
]);
}
public function test_stats_with_different_periods(): void
public function test_stats_no_longer_exposes_named_periods(): void
{
$periods = ['today', 'week', 'month', 'year', 'all'];
foreach ($periods as $period) {
$this
->getJson("/api/v1/dashboard/stats?period={$period}")
->getJson('/api/v1/dashboard/stats')
->assertStatus(200)
->assertJsonMissingPath('data.available_periods')
->assertJsonMissingPath('data.current_period');
}
public function test_stats_honours_an_explicit_from_and_to_range(): void
{
Article::factory()->count(2)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
Article::factory()->create(['created_at' => Carbon::parse('2026-08-05 12:00:00')]);
$this
->getJson('/api/v1/dashboard/stats?from=2026-07-01&to=2026-07-31')
->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'current_period' => $period,
'article_stats' => ['articles_fetched' => 2],
'range' => ['from' => '2026-07-01', 'to' => '2026-07-31'],
],
]);
}
public function test_stats_defaults_to_the_current_day_without_params(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Article::factory()->create(['created_at' => Carbon::parse('2026-07-15 09:00:00')]);
Article::factory()->create(['created_at' => Carbon::parse('2026-07-14 09:00:00')]);
$this
->getJson('/api/v1/dashboard/stats')
->assertStatus(200)
->assertJson([
'data' => [
'article_stats' => ['articles_fetched' => 1],
'range' => ['from' => '2026-07-15', 'to' => '2026-07-15'],
],
]);
}
public function test_stats_rejects_an_inverted_range(): void
{
$this
->getJson('/api/v1/dashboard/stats?from=2026-07-31&to=2026-07-01')
->assertStatus(422);
}
public function test_stats_rejects_a_malformed_date(): void
{
$this
->getJson('/api/v1/dashboard/stats?from=not-a-date&to=2026-07-01')
->assertStatus(422);
}
public function test_stats_with_sample_data(): void
{
// Get initial counts
$initialArticles = Article::count();
$initialFeeds = Feed::count();
$initialChannels = PlatformChannel::count();
$initialRoutes = Route::count();
$initialPublications = ArticlePublication::count();
// Create test data
$feed = Feed::factory()->create(['is_active' => true]);
$channel = PlatformChannel::factory()->create(['is_active' => true]);
$route = Route::factory()->create(['is_active' => true]);
Route::factory()->create(['is_active' => true]);
// Create articles
$articles = Article::factory()->count(3)->create(['feed_id' => $feed->id]);
$articles = Article::factory()->count(3)->create([
'feed_id' => $feed->id,
'created_at' => Carbon::parse('2026-07-10 12:00:00'),
]);
// Publish one article
ArticlePublication::factory()->create([
'article_id' => $articles->first()->id,
'platform_channel_id' => $channel->id,
'published_at' => now(),
'published_at' => Carbon::parse('2026-07-10 13:00:00'),
]);
$response = $this->getJson('/api/v1/dashboard/stats?period=all');
$response = $this->getJson('/api/v1/dashboard/stats?from=2026-07-01&to=2026-07-31');
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'article_stats' => [
'articles_fetched' => $initialArticles + 3,
'articles_published' => $initialPublications + 1,
'articles_fetched' => 3,
'articles_published' => 1,
],
'system_stats' => [
'total_feeds' => Feed::count(),
'total_platform_channels' => PlatformChannel::count(),
'total_routes' => Route::count(),
],
],
]);
// Just verify structure and that we have more items than we started with
$responseData = $response->json('data');
$this->assertGreaterThanOrEqual($initialFeeds + 1, $responseData['system_stats']['total_feeds']);
$this->assertGreaterThanOrEqual($initialChannels + 1, $responseData['system_stats']['total_platform_channels']);
$this->assertGreaterThanOrEqual($initialRoutes + 1, $responseData['system_stats']['total_routes']);
}
public function test_stats_returns_empty_data_with_no_records(): void

View file

@ -0,0 +1,122 @@
<?php
namespace Tests\Feature\Livewire;
use App\Livewire\Dashboard;
use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Livewire\Livewire;
use Tests\TestCase;
class DashboardTest extends TestCase
{
use RefreshDatabase;
public function test_it_defaults_to_the_current_day(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->assertSet('from', '2026-07-15')
->assertSet('to', '2026-07-15');
}
public function test_it_recomputes_stats_when_the_range_changes(): void
{
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
Livewire::test(Dashboard::class)
->set('from', '2026-07-01')
->set('to', '2026-07-31')
->assertSee('3')
->set('from', '2026-08-01')
->set('to', '2026-08-31')
->assertSee('0');
}
public function test_it_populates_the_range_when_a_preset_is_applied(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->call('applyPreset', 'month')
->assertSet('from', '2026-07-01')
->assertSet('to', '2026-07-31');
}
public function test_it_applies_the_all_preset_as_a_bounded_range(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->call('applyPreset', 'all')
->assertSet('to', '2026-07-15');
}
public function test_it_ignores_an_unknown_preset(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->call('applyPreset', 'fortnight')
->assertSet('from', '2026-07-15')
->assertSet('to', '2026-07-15')
->assertHasNoErrors();
}
public function test_it_reports_an_inverted_range_against_the_end_date(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->set('from', '2026-07-31')
->set('to', '2026-07-01')
->assertHasErrors('to');
}
public function test_it_blames_the_start_date_when_the_start_date_is_malformed(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->set('from', 'not-a-date')
->assertHasErrors('from')
->assertHasNoErrors('to');
}
public function test_it_blames_the_end_date_when_the_end_date_is_malformed(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
Livewire::test(Dashboard::class)
->set('to', 'not-a-date')
->assertHasErrors('to')
->assertHasNoErrors('from');
}
public function test_it_reports_zero_stats_for_an_invalid_range(): void
{
Article::factory()->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
Livewire::test(Dashboard::class)
->set('from', '2026-07-01')
->set('to', 'not-a-date')
->assertSet('rangeIsValid', false);
}
public function test_it_marks_a_well_formed_range_as_valid(): void
{
Livewire::test(Dashboard::class)
->set('from', '2026-07-01')
->set('to', '2026-07-31')
->assertSet('rangeIsValid', true);
}
public function test_it_renders_the_preset_buttons(): void
{
Livewire::test(Dashboard::class)
->assertSee('This Month')
->assertSee('All Time');
}
}

View file

@ -2,41 +2,94 @@
namespace Tests\Unit\Services;
use App\Models\Article;
use App\Models\ArticlePublication;
use App\Services\DashboardStatsService;
use Illuminate\Support\Facades\Http;
use App\Support\DateRange;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;
class DashboardStatsServiceTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
use RefreshDatabase;
// Mock HTTP requests to prevent external calls
Http::fake([
'*' => Http::response('', 500),
]);
private function service(): DashboardStatsService
{
return new DashboardStatsService;
}
public function test_get_available_periods_returns_correct_options(): void
public function test_it_counts_articles_fetched_within_the_range(): void
{
$service = new DashboardStatsService;
$periods = $service->getAvailablePeriods();
Article::factory()->count(2)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
Article::factory()->create(['created_at' => Carbon::parse('2026-08-01 12:00:00')]);
$this->assertIsArray($periods);
$this->assertArrayHasKey('today', $periods);
$this->assertArrayHasKey('week', $periods);
$this->assertArrayHasKey('month', $periods);
$this->assertArrayHasKey('year', $periods);
$this->assertArrayHasKey('all', $periods);
$stats = $this->service()->getStats(new DateRange(
Carbon::parse('2026-07-01 00:00:00'),
Carbon::parse('2026-07-31 23:59:59'),
));
$this->assertEquals('Today', $periods['today']);
$this->assertEquals('All Time', $periods['all']);
$this->assertSame(2, $stats['articles_fetched']);
}
public function test_service_instantiation(): void
public function test_it_counts_articles_published_within_the_range(): void
{
$service = new DashboardStatsService;
$this->assertInstanceOf(DashboardStatsService::class, $service);
ArticlePublication::factory()->create(['published_at' => Carbon::parse('2026-07-10 12:00:00')]);
ArticlePublication::factory()->create(['published_at' => Carbon::parse('2026-08-01 12:00:00')]);
$stats = $this->service()->getStats(new DateRange(
Carbon::parse('2026-07-01 00:00:00'),
Carbon::parse('2026-07-31 23:59:59'),
));
$this->assertSame(1, $stats['articles_published']);
}
public function test_it_calculates_the_published_percentage(): void
{
$range = new DateRange(
Carbon::parse('2026-07-01 00:00:00'),
Carbon::parse('2026-07-31 23:59:59'),
);
Article::factory()->count(4)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
ArticlePublication::factory()->create(['published_at' => Carbon::parse('2026-07-10 12:00:00')]);
$stats = $this->service()->getStats($range);
$this->assertSame(25.0, $stats['published_percentage']);
}
public function test_it_reports_a_zero_percentage_when_nothing_was_fetched(): void
{
$stats = $this->service()->getStats(DateRange::preset('all'));
$this->assertSame(0, $stats['articles_fetched']);
$this->assertSame(0.0, $stats['published_percentage']);
}
public function test_it_excludes_records_outside_the_range_entirely(): void
{
Article::factory()->create(['created_at' => Carbon::parse('2026-06-30 23:59:59')]);
Article::factory()->create(['created_at' => Carbon::parse('2026-08-01 00:00:00')]);
$stats = $this->service()->getStats(new DateRange(
Carbon::parse('2026-07-01 00:00:00'),
Carbon::parse('2026-07-31 23:59:59'),
));
$this->assertSame(0, $stats['articles_fetched']);
}
public function test_it_counts_everything_for_the_all_preset(): void
{
Carbon::setTestNow('2026-07-15 12:00:00');
Article::factory()->create(['created_at' => Carbon::parse('2020-01-01 00:00:00')]);
Article::factory()->create(['created_at' => Carbon::parse('2026-07-15 11:00:00')]);
$stats = $this->service()->getStats(DateRange::preset('all'));
$this->assertSame(2, $stats['articles_fetched']);
}
}