create([ 'username' => 'journeymaker', 'is_active' => true, 'created_at' => Carbon::parse('2020-01-02 09:00:00'), ]); $hiddenArtwork = Artwork::factory()->for($creator)->private()->create([ 'title' => 'Hidden Draft', 'slug' => 'hidden-draft', 'published_at' => Carbon::parse('2020-02-01 12:00:00'), ]); $firstArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Sky One', 'slug' => 'sky-one', 'published_at' => Carbon::parse('2021-03-10 10:00:00'), ]); $breakthroughArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Neon Archive', 'slug' => 'neon-archive', 'published_at' => Carbon::parse('2024-05-10 09:00:00'), ]); $lateYearArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Terminal Bloom', 'slug' => 'terminal-bloom', 'published_at' => Carbon::parse('2024-11-20 19:00:00'), ]); $latestArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Afterglow Atlas', 'slug' => 'afterglow-atlas', 'published_at' => Carbon::parse('2025-02-14 18:30:00'), ]); DB::table('artwork_stats')->insert([ [ 'artwork_id' => $hiddenArtwork->id, 'views' => 5000, 'downloads' => 900, 'favorites' => 400, 'rating_avg' => 0, 'rating_count' => 0, 'comments_count' => 35, 'shares_count' => 12, 'downloads_1h' => 80, 'heat_score_updated_at' => null, ], [ 'artwork_id' => $firstArtwork->id, 'views' => 120, 'downloads' => 18, 'favorites' => 9, 'rating_avg' => 0, 'rating_count' => 0, 'comments_count' => 2, 'shares_count' => 0, 'downloads_1h' => 2, 'heat_score_updated_at' => null, ], [ 'artwork_id' => $breakthroughArtwork->id, 'views' => 1800, 'downloads' => 220, 'favorites' => 110, 'rating_avg' => 0, 'rating_count' => 0, 'comments_count' => 26, 'shares_count' => 9, 'downloads_1h' => 24, 'heat_score_updated_at' => Carbon::parse('2024-05-10 11:00:00'), ], [ 'artwork_id' => $lateYearArtwork->id, 'views' => 640, 'downloads' => 90, 'favorites' => 34, 'rating_avg' => 0, 'rating_count' => 0, 'comments_count' => 7, 'shares_count' => 2, 'downloads_1h' => 5, 'heat_score_updated_at' => null, ], [ 'artwork_id' => $latestArtwork->id, 'views' => 450, 'downloads' => 48, 'favorites' => 18, 'rating_avg' => 0, 'rating_count' => 0, 'comments_count' => 4, 'shares_count' => 1, 'downloads_1h' => 3, 'heat_score_updated_at' => null, ], ]); DB::table('artwork_features')->insert([ 'artwork_id' => $firstArtwork->id, 'featured_at' => Carbon::parse('2022-06-01 13:00:00'), 'priority' => 100, 'label' => 'Feature', 'is_active' => true, 'created_by' => $creator->id, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('artwork_metric_snapshots_hourly')->insert([ [ 'artwork_id' => $breakthroughArtwork->id, 'bucket_hour' => Carbon::parse('2024-05-10 10:00:00'), 'views_count' => 1200, 'downloads_count' => 40, 'favourites_count' => 60, 'comments_count' => 12, 'shares_count' => 3, 'created_at' => now(), ], [ 'artwork_id' => $breakthroughArtwork->id, 'bucket_hour' => Carbon::parse('2024-05-10 11:00:00'), 'views_count' => 1320, 'downloads_count' => 64, 'favourites_count' => 68, 'comments_count' => 13, 'shares_count' => 4, 'created_at' => now(), ], [ 'artwork_id' => $lateYearArtwork->id, 'bucket_hour' => Carbon::parse('2024-11-20 19:00:00'), 'views_count' => 300, 'downloads_count' => 10, 'favourites_count' => 12, 'comments_count' => 1, 'shares_count' => 0, 'created_at' => now(), ], [ 'artwork_id' => $lateYearArtwork->id, 'bucket_hour' => Carbon::parse('2024-11-20 20:00:00'), 'views_count' => 360, 'downloads_count' => 14, 'favourites_count' => 15, 'comments_count' => 2, 'shares_count' => 0, 'created_at' => now(), ], ]); $groupOwner = User::factory()->create(['is_active' => true]); $group = Group::factory()->for($groupOwner, 'owner')->create([ 'name' => 'Nova Collective', 'slug' => 'nova-collective', 'visibility' => Group::VISIBILITY_PUBLIC, 'status' => Group::LIFECYCLE_ACTIVE, ]); $release = GroupRelease::query()->create([ 'group_id' => $group->id, 'title' => 'First Spectrum Pack', 'slug' => 'first-spectrum-pack', 'summary' => 'A first collaborative release.', 'status' => GroupRelease::STATUS_RELEASED, 'current_stage' => GroupRelease::STAGE_RELEASED, 'visibility' => GroupRelease::VISIBILITY_PUBLIC, 'released_at' => Carbon::parse('2023-08-15 16:00:00'), 'published_at' => Carbon::parse('2023-08-15 16:00:00'), 'created_by_user_id' => $groupOwner->id, ]); GroupReleaseContributor::query()->create([ 'group_release_id' => $release->id, 'user_id' => $creator->id, 'role_label' => 'Illustrator', 'sort_order' => 1, ]); return $creator; } it('rebuilds persisted creator milestones from public source data', function () { $creator = seedCreatorJourneyFixture(); $this->artisan('skinbase:rebuild-creator-journey', ['user_id' => $creator->id]) ->assertExitCode(0); $storedTypes = DB::table('creator_milestones') ->where('user_id', $creator->id) ->orderBy('type') ->pluck('type') ->all(); expect($storedTypes)->toContain( 'first_upload', 'first_featured_artwork', 'first_group_release', 'biggest_download_spike', 'best_performing_work', 'most_productive_year', 'yearly_recap', ); $payload = app(CreatorJourneyService::class)->publicPayloadForUser($creator); expect($payload['summary']['available'])->toBeTrue() ->and($payload['summary']['member_since_year'])->toBe(2020) ->and($payload['highlights'][0]['type'])->toBe('best_performing_work') ->and(collect($payload['timeline'])->pluck('headline')->all())->toContain('Sky One', 'First Spectrum Pack') ->and(collect($payload['timeline'])->pluck('headline')->all())->not->toContain('Hidden Draft') ->and($payload['yearly_recaps'][0]['metrics']['year'])->toBe(2025); }); it('returns the public creator journey api payload without leaking private content', function () { $creator = seedCreatorJourneyFixture(); app(CreatorJourneyService::class)->rebuildForUser($creator); $response = $this->getJson(route('api.profile.journey', ['username' => $creator->username])); $response ->assertOk() ->assertJsonPath('data.summary.available', true); // v2: comeback milestones appear in timeline (fixture has a 3+ year gap → legendary comeback) $highlightHeadlines = collect($response->json('data.highlights'))->pluck('headline')->filter()->all(); $headlines = collect($response->json('data.timeline'))->pluck('headline')->filter()->all(); $timelineTypes = collect($response->json('data.timeline'))->pluck('type')->filter()->values()->all(); expect($highlightHeadlines)->toContain('Neon Archive') ->and($headlines)->toContain('Sky One') ->and($headlines)->not->toContain('Hidden Draft') ->and($timelineTypes)->toContain('biggest_download_spike'); }); it('hydrates the public profile page with creator journey props', function () { $creator = seedCreatorJourneyFixture(); app(CreatorJourneyService::class)->rebuildForUser($creator); $this->get(route('profile.show', ['username' => strtolower((string) $creator->username)])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Profile/ProfileShow') ->where('journey.summary.available', true) ->where('journey.summary.member_since_year', 2020) ->where('journey.highlights', fn ($highlights) => collect($highlights)->pluck('headline')->contains('Neon Archive')) ->where('journey.timeline', fn ($timeline) => collect($timeline)->pluck('type')->contains('biggest_download_spike')) ->where('journey.timeline', fn ($timeline) => collect($timeline)->pluck('headline')->contains('First Spectrum Pack')) ->where('journeyApiUrl', route('api.profile.journey', ['username' => strtolower((string) $creator->username)])) ); });