create(); $artwork = Artwork::factory()->create(); $response = $this->actingAs($user)->postJson('/api/analytics/feed', [ 'event_type' => 'feed_click', 'artwork_id' => $artwork->id, 'position' => 3, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => 27, ]); $response->assertOk()->assertJson(['success' => true]); $this->assertDatabaseHas('feed_events', [ 'user_id' => $user->id, 'artwork_id' => $artwork->id, 'event_type' => 'feed_click', 'position' => 3, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => 27, ]); }); it('aggregates daily feed analytics with ctr save-rate and dwell buckets', function () { $user = User::factory()->create(); $artworkA = Artwork::factory()->create(); $artworkB = Artwork::factory()->create(); $metricDate = now()->subDay()->toDateString(); DB::table('feed_events')->insert([ [ 'event_date' => $metricDate, 'event_type' => 'feed_impression', 'user_id' => $user->id, 'artwork_id' => $artworkA->id, 'position' => 1, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => null, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], [ 'event_date' => $metricDate, 'event_type' => 'feed_impression', 'user_id' => $user->id, 'artwork_id' => $artworkB->id, 'position' => 2, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => null, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], [ 'event_date' => $metricDate, 'event_type' => 'feed_click', 'user_id' => $user->id, 'artwork_id' => $artworkA->id, 'position' => 1, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => 3, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], [ 'event_date' => $metricDate, 'event_type' => 'feed_click', 'user_id' => $user->id, 'artwork_id' => $artworkB->id, 'position' => 2, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => 35, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], ]); DB::table('user_discovery_events')->insert([ 'event_id' => '33333333-3333-3333-3333-333333333333', 'user_id' => $user->id, 'artwork_id' => $artworkA->id, 'category_id' => null, 'event_type' => 'favorite', 'event_version' => 'event-v1', 'algo_version' => 'clip-cosine-v1', 'weight' => 1, 'event_date' => $metricDate, 'occurred_at' => now()->subDay(), 'meta' => null, 'created_at' => now(), 'updated_at' => now(), ]); $this->artisan('analytics:aggregate-feed', ['--date' => $metricDate])->assertSuccessful(); $this->assertDatabaseHas('feed_daily_metrics', [ 'metric_date' => $metricDate, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'impressions' => 2, 'clicks' => 2, 'saves' => 1, 'dwell_0_5' => 1, 'dwell_30_120' => 1, ]); $metric = DB::table('feed_daily_metrics') ->where('metric_date', $metricDate) ->where('algo_version', 'clip-cosine-v1') ->where('source', 'personalized') ->first(); expect((float) $metric->ctr)->toBe(1.0); expect((float) $metric->save_rate)->toBe(0.5); });