*/ private const GENRE_SPECS = [ ['name' => 'Hardstyle', 'color' => '#e85d75'], ['name' => 'Techno', 'color' => '#1f6feb'], ['name' => 'Indie', 'color' => '#7c3aed'], ['name' => 'Live band', 'color' => '#0ea5e9'], ['name' => 'House', 'color' => '#22c55e'], ['name' => 'Hip-hop', 'color' => '#f59e0b'], ['name' => 'Drum & Bass', 'color' => '#dc2626'], ['name' => 'Akoestisch', 'color' => '#6366f1'], ]; // ========================================================================= // Public entry points // ========================================================================= /** * Seed an organisation-level roster: 8 genres + N artists. * * @return array{genres: Collection, artists: Collection} */ public static function seedOrganisationPool(Organisation $org, int $artistCount = 125): array { $genres = self::createGenres($org); $artists = self::createArtistPool($org, $genres, $artistCount); return ['genres' => $genres, 'artists' => $artists]; } /** * Seed Echt Feesten festival fixture: 5 stages, ~30 scheduled * engagements over 3 days, ~10 unscheduled engagements. Preserves * the prototype audit fixture's special test cases (B2B pair on * Mainstage Saturday, multi-perf engagement, parked performance). * * @param array $subEvents Sub-events keyed 0..n in date order * @param array{genres: Collection, artists: Collection} $pool * @return array{stages: int, engagements: int, performances: int, unscheduled: int} */ public static function seedForFestival(Organisation $org, Event $festival, array $subEvents, array $pool): array { if (count($subEvents) < 3) { return ['stages' => 0, 'engagements' => 0, 'performances' => 0, 'unscheduled' => 0]; } $stages = self::createStages($festival, [ ['name' => 'Mainstage', 'color' => '#e85d75', 'capacity' => 4500, 'sort_order' => 1], ['name' => 'Havana', 'color' => '#0ea5e9', 'capacity' => 1200, 'sort_order' => 2], ['name' => 'Stairway', 'color' => '#7c3aed', 'capacity' => 800, 'sort_order' => 3], ['name' => 'Socialite', 'color' => '#22c55e', 'capacity' => 600, 'sort_order' => 4], ['name' => 'Tent op het Plein', 'color' => '#f59e0b', 'capacity' => 400, 'sort_order' => 5], ], $subEvents); // Reserve the first 50 artists from the org pool for festival // engagements. (Other events draw from later slices to keep // some artists reserved for "unscheduled" / pure-pool variety.) $bucket = $pool['artists']->slice(0, 50)->values(); $bucketIdx = 0; $engagements = 0; $performances = 0; $unscheduled = 0; // ── Scheduled engagements: 30 across 3 days ── // Status mix loosely follows RFC §5.3: lots of contracted/confirmed, // some optioned/requested, a couple of drafts and one cancelled. $statusMix = [ ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Option, ArtistEngagementStatus::Option, ArtistEngagementStatus::Requested, ]; $statusIdx = 0; $hourSlots = [16, 17, 18, 19, 20, 21, 22, 23]; foreach ($subEvents as $dayIdx => $subEvent) { // Each sub-event gets 10 scheduled engagements across the 5 stages for ($i = 0; $i < 10; $i++) { $artist = $bucket[$bucketIdx++ % $bucket->count()]; $status = $statusMix[$statusIdx++ % count($statusMix)]; // unique(artist_id, event_id): only one engagement per // (artist, sub-event). Since we wrap the pool, skip if // we'd violate the constraint. $existing = ArtistEngagement::query() ->where('artist_id', $artist->id) ->where('event_id', $subEvent->id) ->exists(); if ($existing) { continue; } $engagement = self::createEngagement($artist, $subEvent, $status); $engagements++; $hour = $hourSlots[$i % count($hourSlots)]; $minute = ($i % 4) * 15; $stage = $stages->values()[$i % $stages->count()]; self::createPerformance($engagement, $subEvent, $stage, $hour, $minute, 60); $performances++; // ~25% of contracted/confirmed engagements get a 2nd perf (D17 multi-perf) if ($i % 4 === 0 && in_array($status, [ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Confirmed], true)) { self::createPerformance($engagement, $subEvent, $stage, $hour + 4, $minute, 45); $performances++; } } } // ── Unscheduled engagements: 12 artists linked but no performance ── $unscheduledStatusMix = [ ArtistEngagementStatus::Draft, ArtistEngagementStatus::Draft, ArtistEngagementStatus::Requested, ArtistEngagementStatus::Requested, ArtistEngagementStatus::Option, ArtistEngagementStatus::Option, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Offered, ]; $u = 0; for ($i = 0; $i < 12; $i++) { $artist = $bucket[$bucketIdx++ % $bucket->count()]; $subEvent = $subEvents[$i % count($subEvents)]; $existing = ArtistEngagement::query() ->where('artist_id', $artist->id) ->where('event_id', $subEvent->id) ->exists(); if ($existing) { continue; } self::createEngagement($artist, $subEvent, $unscheduledStatusMix[$u++ % count($unscheduledStatusMix)]); $engagements++; $unscheduled++; } // ── One parked performance (= scheduled but stage_id = null) ── // Exercises the Session 4 "wachtrij" UI. $parkedArtist = $bucket[$bucketIdx++ % $bucket->count()]; $parkedSub = $subEvents[0]; if (! ArtistEngagement::query()->where('artist_id', $parkedArtist->id)->where('event_id', $parkedSub->id)->exists()) { $parkedEngagement = self::createEngagement($parkedArtist, $parkedSub, ArtistEngagementStatus::Confirmed); Performance::create([ 'engagement_id' => $parkedEngagement->id, 'event_id' => $parkedSub->id, 'stage_id' => null, 'lane' => 0, 'start_at' => CarbonImmutable::parse($parkedSub->start_date)->setTime(0, 0), 'end_at' => CarbonImmutable::parse($parkedSub->start_date)->setTime(1, 0), 'version' => 0, ]); $engagements++; $performances++; } // ── B2B pair on Mainstage Saturday with 3-min offset (B2B detector seed) ── // Two artists overlap on the same stage/lane within 3 minutes. $zaterdag = $subEvents[1]; $b2bA = $bucket[$bucketIdx++ % $bucket->count()]; $b2bB = $bucket[$bucketIdx++ % $bucket->count()]; $mainstage = $stages['Mainstage'] ?? $stages->first(); foreach ([['artist' => $b2bA, 'minute' => 30], ['artist' => $b2bB, 'minute' => 33]] as $pair) { if (ArtistEngagement::query()->where('artist_id', $pair['artist']->id)->where('event_id', $zaterdag->id)->exists()) { continue; } $eng = self::createEngagement($pair['artist'], $zaterdag, ArtistEngagementStatus::Contracted); self::createPerformance($eng, $zaterdag, $mainstage, 23, $pair['minute'], 60); $engagements++; $performances++; } return ['stages' => $stages->count(), 'engagements' => $engagements, 'performances' => $performances, 'unscheduled' => $unscheduled]; } /** * Seed a flat single-day event (e.g. Braderie, Koningsdag, Nacht). * * @param array{genres: Collection, artists: Collection} $pool * @param array{ * stages?: list, * scheduled?: int, * unscheduled?: int, * pool_offset?: int, * start_hour?: int, * end_hour?: int, * force_status?: ArtistEngagementStatus * } $config * @return array{stages: int, engagements: int, performances: int, unscheduled: int} */ public static function seedForFlatEvent(Organisation $org, Event $event, array $pool, array $config = []): array { $stageSpecs = $config['stages'] ?? [ ['name' => 'Hoofdpodium', 'color' => '#e85d75', 'capacity' => 1500, 'sort_order' => 1], ['name' => 'Tweede Podium', 'color' => '#0ea5e9', 'capacity' => 600, 'sort_order' => 2], ]; $stages = self::createStages($event, $stageSpecs, [$event]); $scheduledCount = $config['scheduled'] ?? 10; $unscheduledCount = $config['unscheduled'] ?? 5; $offset = $config['pool_offset'] ?? 50; $startHour = $config['start_hour'] ?? 14; $endHour = $config['end_hour'] ?? 23; $forceStatus = $config['force_status'] ?? null; $bucket = $pool['artists']->slice($offset, $scheduledCount + $unscheduledCount + 4)->values(); $bucketIdx = 0; $engagements = 0; $performances = 0; $unscheduled = 0; $statusMix = $forceStatus !== null ? [$forceStatus] : [ ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Option, ArtistEngagementStatus::Requested, ]; $statusIdx = 0; $totalHours = max(1, $endHour - $startHour); for ($i = 0; $i < $scheduledCount; $i++) { $artist = $bucket[$bucketIdx++ % $bucket->count()]; if (ArtistEngagement::query()->where('artist_id', $artist->id)->where('event_id', $event->id)->exists()) { continue; } $status = $statusMix[$statusIdx++ % count($statusMix)]; $engagement = self::createEngagement($artist, $event, $status); $engagements++; $stage = $stages->values()[$i % $stages->count()]; $hour = $startHour + ($i % $totalHours); $minute = ($i % 4) * 15; self::createPerformance($engagement, $event, $stage, $hour, $minute, 60); $performances++; } $unscheduledStatusMix = $forceStatus !== null ? [$forceStatus] : [ ArtistEngagementStatus::Draft, ArtistEngagementStatus::Requested, ArtistEngagementStatus::Option, ArtistEngagementStatus::Confirmed, ]; for ($i = 0; $i < $unscheduledCount; $i++) { $artist = $bucket[$bucketIdx++ % $bucket->count()]; if (ArtistEngagement::query()->where('artist_id', $artist->id)->where('event_id', $event->id)->exists()) { continue; } self::createEngagement($artist, $event, $unscheduledStatusMix[$i % count($unscheduledStatusMix)]); $engagements++; $unscheduled++; } return ['stages' => $stages->count(), 'engagements' => $engagements, 'performances' => $performances, 'unscheduled' => $unscheduled]; } /** * Seed a recurring series (e.g. IJsbaan): stages live on the parent * event, and each weekly sub-event gets its own scheduled + * unscheduled engagements. * * @param array $subEvents * @param array{genres: Collection, artists: Collection} $pool * @param array{ * stages?: list, * per_week_scheduled?: int, * per_week_unscheduled?: int, * pool_offset?: int * } $config * @return array{stages: int, engagements: int, performances: int, unscheduled: int} */ public static function seedForSeries(Organisation $org, Event $parent, array $subEvents, array $pool, array $config = []): array { $stageSpecs = $config['stages'] ?? [ ['name' => 'Schaatspaviljoen', 'color' => '#0ea5e9', 'capacity' => 800, 'sort_order' => 1], ['name' => 'Verwarmde Tent', 'color' => '#22c55e', 'capacity' => 300, 'sort_order' => 2], ]; $stages = self::createStages($parent, $stageSpecs, $subEvents); $perWeekScheduled = $config['per_week_scheduled'] ?? 3; $perWeekUnscheduled = $config['per_week_unscheduled'] ?? 2; $offset = $config['pool_offset'] ?? 80; $bucket = $pool['artists']->slice($offset, ($perWeekScheduled + $perWeekUnscheduled) * count($subEvents) + 4)->values(); $bucketIdx = 0; $engagements = 0; $performances = 0; $unscheduled = 0; foreach ($subEvents as $weekIdx => $week) { for ($i = 0; $i < $perWeekScheduled; $i++) { $artist = $bucket[$bucketIdx++ % $bucket->count()]; if (ArtistEngagement::query()->where('artist_id', $artist->id)->where('event_id', $week->id)->exists()) { continue; } $status = [ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Option][$i % 3]; $engagement = self::createEngagement($artist, $week, $status); $engagements++; $stage = $stages->values()[$i % $stages->count()]; self::createPerformance($engagement, $week, $stage, 14 + $i * 2, 0, 60); $performances++; } for ($i = 0; $i < $perWeekUnscheduled; $i++) { $artist = $bucket[$bucketIdx++ % $bucket->count()]; if (ArtistEngagement::query()->where('artist_id', $artist->id)->where('event_id', $week->id)->exists()) { continue; } $status = [ArtistEngagementStatus::Draft, ArtistEngagementStatus::Requested, ArtistEngagementStatus::Option][$i % 3]; self::createEngagement($artist, $week, $status); $engagements++; $unscheduled++; } } return ['stages' => $stages->count(), 'engagements' => $engagements, 'performances' => $performances, 'unscheduled' => $unscheduled]; } // ========================================================================= // Internal helpers // ========================================================================= /** @return Collection */ private static function createGenres(Organisation $org): Collection { $genres = collect(); foreach (self::GENRE_SPECS as $i => $spec) { $genres[$spec['name']] = Genre::create([ 'organisation_id' => $org->id, 'name' => $spec['name'], 'color' => $spec['color'], 'sort_order' => $i + 1, 'is_active' => true, ]); } return $genres; } /** * @param Collection $genres * @return Collection */ private static function createArtistPool(Organisation $org, Collection $genres, int $count): Collection { $names = self::generateArtistNames($count); $countries = ['NL', 'NL', 'NL', 'NL', 'NL', 'BE', 'BE', 'DE', 'UK', 'FR']; $genreList = $genres->values(); $artists = collect(); foreach ($names as $idx => $name) { $genre = $genreList[$idx % $genreList->count()]; $artist = Artist::create([ 'organisation_id' => $org->id, 'name' => $name, 'default_genre_id' => $genre->id, 'default_draw' => fake()->numberBetween(50, 5000), 'star_rating' => fake()->numberBetween(1, 5), 'home_base_country' => $countries[$idx % count($countries)], ]); $artists->push($artist); // ~30% get a tour-manager contact for downstream advance/portal flows if ($idx % 3 === 0) { ArtistContact::create([ 'artist_id' => $artist->id, 'name' => 'Tour Manager '.$artist->name, 'email' => 'tm-'.$artist->slug.'@example.test', 'phone' => '+316123'.str_pad((string) ($idx + 1), 5, '0', STR_PAD_LEFT), 'role' => 'tour_manager', 'is_primary' => true, 'receives_briefing' => true, 'receives_infosheet' => true, ]); } } return $artists; } /** * Generate $count unique artist names by combining curated parts. * The first ~40 entries are hand-picked "real-feeling" names so the * top of the list looks human; the remainder are generated. * * @return list */ private static function generateArtistNames(int $count): array { $curated = [ 'Donker & Licht', 'Voltage Collective', 'Roos & de Wolf', 'Rotterdam Brass', 'Nachtwacht DJs', 'De Lichtbrigade', 'Dijkdoorbraak', 'Kaapse Gasten', 'Polderpop', 'De Stadsklokken', 'Noorderzon Project', 'Bij de Buren', 'Echo van de Maas', 'Kortsluiting', 'Bonte Hond', 'Rauwe Diamant', 'Maan & Sterren', 'Zondagmiddag Sessies', 'Het Geluidsmuseum', 'Storm op Zee', 'De Jonge Honden', 'Volksfeest Brass Band', 'Tussen de Wolken', 'Plat Vlaams', 'Stille Helden', 'De Veerboot', 'Holland Heat', 'Springstof', 'Café Onder de Brug', 'Mannen van Staal', 'Dijk Disco', 'Studio West', 'Het Kleine Orkest', 'Hard tegen Hart', 'Nachtsessie Live', 'Boterham met Tien', 'De Klompendansers', 'Kraakheldere Stemmen', 'Vrijdagavondblues', 'Broeders Beeld', ]; $cores = [ 'Voltage', 'Echo', 'Lumen', 'Donker', 'Licht', 'Storm', 'Zon', 'Maan', 'Stadse', 'Polder', 'Noord', 'Zuid', 'Oost', 'West', 'Maas', 'IJssel', 'Rotterdam', 'Amsterdam', 'Utrecht', 'Eindhoven', 'Brabant', 'Limburg', 'Kraak', 'Veer', 'Brug', 'Plein', 'Markt', 'Park', 'Tuin', 'Bos', 'Ster', 'Wolk', 'Regen', 'Wind', 'Vuur', 'IJs', 'Sneeuw', 'Mist', ]; $suffixes = [ 'Collective', 'Project', 'Sound', 'Live', 'Crew', 'Brothers', 'Sisters', 'Sessions', 'Orkest', 'Band', 'Trio', 'Quartet', 'DJs', 'System', 'Allstars', 'Republic', 'Union', 'Express', 'Riders', 'Beats', ]; $names = $curated; $taken = array_flip($names); $i = 0; while (count($names) < $count) { $core = $cores[$i % count($cores)]; $suffix = $suffixes[(int) ($i / count($cores)) % count($suffixes)]; $candidate = $core.' '.$suffix; if (! isset($taken[$candidate])) { $names[] = $candidate; $taken[$candidate] = true; } $i++; // Safety: avoid infinite loop if combinations exhaust if ($i > count($cores) * count($suffixes) * 2) { $names[] = $core.' '.$suffix.' '.Str::upper(Str::random(3)); } } return array_slice($names, 0, $count); } /** * Create stages on $event and stage_days entries linking each stage * to every $performanceDay (the parent event itself for flat events, * each sub-event for festivals/series). * * @param list $specs * @param array $performanceDays * @return Collection */ private static function createStages(Event $event, array $specs, array $performanceDays): Collection { $stages = collect(); foreach ($specs as $spec) { $stage = Stage::create([ 'event_id' => $event->id, 'name' => $spec['name'], 'color' => $spec['color'], 'capacity' => $spec['capacity'], 'sort_order' => $spec['sort_order'], ]); $stages[$spec['name']] = $stage; foreach ($performanceDays as $day) { StageDay::create([ 'stage_id' => $stage->id, 'event_id' => $day->id, ]); } } return $stages; } private static function createEngagement(Artist $artist, Event $event, ArtistEngagementStatus $status): ArtistEngagement { $attrs = [ 'artist_id' => $artist->id, 'event_id' => $event->id, 'booking_status' => $status, 'fee_currency' => 'EUR', 'buma_applicable' => true, 'buma_percentage' => 7.00, 'buma_handled_by' => 'organisation', 'vat_applicable' => true, 'vat_percentage' => 21.00, 'payment_status' => 'none', 'crew_count' => fake()->numberBetween(1, 4), 'guests_count' => fake()->numberBetween(0, 6), 'advancing_completed_count' => 0, 'advancing_total_count' => 0, ]; if ($status === ArtistEngagementStatus::Option) { $attrs['option_expires_at'] = CarbonImmutable::now()->addDays(fake()->numberBetween(7, 30)); } if ($status === ArtistEngagementStatus::Requested) { $attrs['requested_at'] = CarbonImmutable::now()->subDays(fake()->numberBetween(1, 14)); } if ($status === ArtistEngagementStatus::Contracted) { $attrs['fee_amount'] = fake()->randomFloat(2, 750, 18000); } if ($status === ArtistEngagementStatus::Confirmed) { $attrs['fee_amount'] = fake()->randomFloat(2, 500, 12000); } return ArtistEngagement::create($attrs); } private static function createPerformance( ArtistEngagement $engagement, Event $day, Stage $stage, int $hour, int $minute, int $minutes, ): Performance { $dayOffset = intdiv($hour, 24); $start = CarbonImmutable::parse($day->start_date) ->addDays($dayOffset) ->setTime($hour % 24, $minute); return Performance::create([ 'engagement_id' => $engagement->id, 'event_id' => $day->id, 'stage_id' => $stage->id, 'lane' => 0, 'start_at' => $start, 'end_at' => $start->addMinutes($minutes), 'version' => 0, ]); } }