'null']); }); // ── Helper: ensure a stats row exists ──────────────────────────────────────── function seedStats(int $artworkId, array $overrides = []): void { DB::table('artwork_stats')->insertOrIgnore(array_merge([ 'artwork_id' => $artworkId, 'views' => 0, 'views_24h' => 0, 'views_7d' => 0, 'downloads' => 0, 'downloads_24h' => 0, 'downloads_7d' => 0, 'favorites' => 0, 'rating_avg' => 0, 'rating_count' => 0, ], $overrides)); } // ── ArtworkStatsService ─────────────────────────────────────────────────────── it('incrementViews updates views, views_24h, and views_7d', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay()]); seedStats($artwork->id); app(ArtworkStatsService::class)->incrementViews($artwork->id, 3, defer: false); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); expect((int) $row->views)->toBe(3); expect((int) $row->views_24h)->toBe(3); expect((int) $row->views_7d)->toBe(3); }); it('incrementDownloads updates downloads, downloads_24h, and downloads_7d', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay()]); seedStats($artwork->id); app(ArtworkStatsService::class)->incrementDownloads($artwork->id, 2, defer: false); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); expect((int) $row->downloads)->toBe(2); expect((int) $row->downloads_24h)->toBe(2); expect((int) $row->downloads_7d)->toBe(2); }); it('multiple view increments accumulate across all three columns', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay()]); seedStats($artwork->id); $svc = app(ArtworkStatsService::class); $svc->incrementViews($artwork->id, 1, defer: false); $svc->incrementViews($artwork->id, 1, defer: false); $svc->incrementViews($artwork->id, 1, defer: false); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); expect((int) $row->views)->toBe(3); expect((int) $row->views_24h)->toBe(3); expect((int) $row->views_7d)->toBe(3); }); // ── ResetWindowedStatsCommand ───────────────────────────────────────────────── it('reset-windowed-stats --period=24h zeros views_24h', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay()]); seedStats($artwork->id, ['views_24h' => 50, 'views_7d' => 200]); $this->artisan('skinbase:reset-windowed-stats', ['--period' => '24h']) ->assertExitCode(0); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); expect((int) $row->views_24h)->toBe(0); // 7d column is NOT touched by a 24h reset expect((int) $row->views_7d)->toBe(200); }); it('reset-windowed-stats --period=7d zeros views_7d', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay()]); seedStats($artwork->id, ['views_24h' => 50, 'views_7d' => 200]); $this->artisan('skinbase:reset-windowed-stats', ['--period' => '7d']) ->assertExitCode(0); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); expect((int) $row->views_7d)->toBe(0); // 24h column is NOT touched by a 7d reset expect((int) $row->views_24h)->toBe(50); }); it('reset-windowed-stats recomputes downloads_24h from artwork_downloads log', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay()]); seedStats($artwork->id, ['downloads_24h' => 99]); // stale value // Insert 3 downloads within the last 24 hours $ip = inet_pton('127.0.0.1'); DB::table('artwork_downloads')->insert([ ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subHours(1)], ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subHours(6)], ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subHours(12)], ]); // Insert 2 old downloads outside the 24h window DB::table('artwork_downloads')->insert([ ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subDays(2)], ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subDays(5)], ]); $this->artisan('skinbase:reset-windowed-stats', ['--period' => '24h']) ->assertExitCode(0); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); // Should equal exactly the 3 recent downloads, not the stale 99 expect((int) $row->downloads_24h)->toBe(3); }); it('reset-windowed-stats recomputes downloads_7d including all downloads in 7-day window', function () { $artwork = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subDays(10)]); seedStats($artwork->id, ['downloads_7d' => 0]); $ip = inet_pton('127.0.0.1'); DB::table('artwork_downloads')->insert([ ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subDays(1)], ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subDays(5)], ['artwork_id' => $artwork->id, 'user_id' => null, 'ip' => $ip, 'user_agent' => null, 'created_at' => now()->subDays(8)], // outside 7d ]); $this->artisan('skinbase:reset-windowed-stats', ['--period' => '7d']) ->assertExitCode(0); $row = DB::table('artwork_stats')->where('artwork_id', $artwork->id)->first(); expect((int) $row->downloads_7d)->toBe(2); }); it('reset-windowed-stats returns failure for invalid period', function () { $this->artisan('skinbase:reset-windowed-stats', ['--period' => 'bad']) ->assertExitCode(1); });