Add addtional test data using seeders for Artist Management module
This commit is contained in:
@@ -15,195 +15,559 @@ use App\Models\Performance;
|
||||
use App\Models\Stage;
|
||||
use App\Models\StageDay;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Seeds the Artist & Timetable fixture for an existing festival with
|
||||
* three sub-events (e.g. Echt Feesten 2026 → Vrijdag/Zaterdag/Zondag).
|
||||
* Seeds the org-wide artist roster + per-event timetable fixtures.
|
||||
*
|
||||
* Reproduces the prototype audit fixture as closely as possible:
|
||||
* - 4 stages on the festival
|
||||
* - 4 stages × 3 sub-events = 12 stage_days rows
|
||||
* - 4 genres, 6 master artists with default genre + draw
|
||||
* - 12 engagements with status mix (Draft 1, Requested 2, Option 3,
|
||||
* Confirmed 2, Contracted 3, Cancelled 1). Two artists each have
|
||||
* two engagements (one per day) to exercise the multi-engagement
|
||||
* case (D17).
|
||||
* - 13 performances; one parked (stage_id=null); a B2B pair within
|
||||
* 3 minutes on the same stage/lane to seed Session 4 frontend dev.
|
||||
* - One ArtistContact per artist (tour-manager role).
|
||||
* Three entry points:
|
||||
* - seedOrganisationPool() — 8 genres + 125 artists at the organisation level
|
||||
* - seedForFestival() — multi-day festival with sub-events (Echt Feesten)
|
||||
* - seedForFlatEvent() — single-day event (Braderie, Koningsdag, Nacht)
|
||||
* - seedForSeries() — recurring series with weekly sub-events (IJsbaan)
|
||||
*
|
||||
* advance_sections seeding lands in Session 3 with the form-builder
|
||||
* integration.
|
||||
* Every event seeded gets:
|
||||
* - 2–5 stages (with stage_days for active days)
|
||||
* - "scheduled" engagements: artists with one or more performances on a stage
|
||||
* - "unscheduled" engagements: artists linked to the event but without
|
||||
* any performance row yet (= booked but not slotted in the timetable)
|
||||
*/
|
||||
final class ArtistTimetableDevSeeder
|
||||
{
|
||||
/** @param array<int, Event> $subEvents Sub-events keyed 0..n in date order */
|
||||
public static function seedForFestival(Organisation $org, Event $festival, array $subEvents): void
|
||||
/**
|
||||
* Curated genre palette used across the org pool.
|
||||
*
|
||||
* @var list<array{name: string, color: string}>
|
||||
*/
|
||||
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<string, Genre>, artists: Collection<int, Artist>}
|
||||
*/
|
||||
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<int, Event> $subEvents Sub-events keyed 0..n in date order
|
||||
* @param array{genres: Collection<string, Genre>, artists: Collection<int, Artist>} $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;
|
||||
return ['stages' => 0, 'engagements' => 0, 'performances' => 0, 'unscheduled' => 0];
|
||||
}
|
||||
|
||||
// Genres
|
||||
$genres = collect([
|
||||
['name' => 'Hardstyle', 'color' => '#e85d75'],
|
||||
['name' => 'Techno', 'color' => '#1f6feb'],
|
||||
['name' => 'Indie', 'color' => '#7c3aed'],
|
||||
['name' => 'Live band', 'color' => '#0ea5e9'],
|
||||
])->mapWithKeys(fn (array $g) => [
|
||||
$g['name'] => Genre::create([
|
||||
'organisation_id' => $org->id,
|
||||
'name' => $g['name'],
|
||||
'color' => $g['color'],
|
||||
'sort_order' => 0,
|
||||
'is_active' => true,
|
||||
]),
|
||||
]);
|
||||
|
||||
// Stages on the festival
|
||||
$stages = collect([
|
||||
$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],
|
||||
])->mapWithKeys(fn (array $s) => [
|
||||
$s['name'] => Stage::create(['event_id' => $festival->id, ...$s]),
|
||||
]);
|
||||
['name' => 'Tent op het Plein', 'color' => '#f59e0b', 'capacity' => 400, 'sort_order' => 5],
|
||||
], $subEvents);
|
||||
|
||||
// stage_days — every stage active every sub-event day
|
||||
foreach ($stages as $stage) {
|
||||
foreach ($subEvents as $subEvent) {
|
||||
StageDay::create([
|
||||
'stage_id' => $stage->id,
|
||||
'event_id' => $subEvent->id,
|
||||
// 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<string, Genre>, artists: Collection<int, Artist>} $pool
|
||||
* @param array{
|
||||
* stages?: list<array{name: string, color: string, capacity: int, sort_order: int}>,
|
||||
* 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<int, Event> $subEvents
|
||||
* @param array{genres: Collection<string, Genre>, artists: Collection<int, Artist>} $pool
|
||||
* @param array{
|
||||
* stages?: list<array{name: string, color: string, capacity: int, sort_order: int}>,
|
||||
* 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<string, Genre> */
|
||||
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<string, Genre> $genres
|
||||
* @return Collection<int, Artist>
|
||||
*/
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Master artists
|
||||
$artistsData = [
|
||||
['name' => 'Donker & Licht', 'genre' => 'Hardstyle', 'draw' => 3500, 'star' => 5],
|
||||
['name' => 'Voltage Collective', 'genre' => 'Techno', 'draw' => 1800, 'star' => 4],
|
||||
['name' => 'Roos & de Wolf', 'genre' => 'Indie', 'draw' => 700, 'star' => 3],
|
||||
['name' => 'Rotterdam Brass', 'genre' => 'Live band', 'draw' => 900, 'star' => 4],
|
||||
['name' => 'Nachtwacht DJs', 'genre' => 'Techno', 'draw' => 1100, 'star' => 3],
|
||||
['name' => 'De Lichtbrigade', 'genre' => 'Live band', 'draw' => 500, 'star' => 3],
|
||||
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<string>
|
||||
*/
|
||||
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',
|
||||
];
|
||||
|
||||
$artists = [];
|
||||
foreach ($artistsData as $data) {
|
||||
/** @var Artist $artist */
|
||||
$artist = Artist::create([
|
||||
'organisation_id' => $org->id,
|
||||
'name' => $data['name'],
|
||||
'default_genre_id' => $genres[$data['genre']]->id,
|
||||
'default_draw' => $data['draw'],
|
||||
'star_rating' => $data['star'],
|
||||
'home_base_country' => 'NL',
|
||||
]);
|
||||
$artists[$data['name']] = $artist;
|
||||
|
||||
ArtistContact::create([
|
||||
'artist_id' => $artist->id,
|
||||
'name' => 'Tour Manager '.$artist->name,
|
||||
'email' => 'tm-'.$artist->slug.'@example.test',
|
||||
'phone' => '+31612340000',
|
||||
'role' => 'tour_manager',
|
||||
'is_primary' => true,
|
||||
'receives_briefing' => true,
|
||||
'receives_infosheet' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// Engagements (12) — status mix per RFC §5.3 Session 1 prompt.
|
||||
// Two artists get two engagements (different days) to exercise D17.
|
||||
$statusPlan = [
|
||||
['artist' => 'Donker & Licht', 'sub' => 0, 'status' => ArtistEngagementStatus::Contracted],
|
||||
['artist' => 'Donker & Licht', 'sub' => 1, 'status' => ArtistEngagementStatus::Contracted],
|
||||
['artist' => 'Voltage Collective', 'sub' => 0, 'status' => ArtistEngagementStatus::Confirmed],
|
||||
['artist' => 'Voltage Collective', 'sub' => 1, 'status' => ArtistEngagementStatus::Option],
|
||||
['artist' => 'Roos & de Wolf', 'sub' => 1, 'status' => ArtistEngagementStatus::Contracted],
|
||||
['artist' => 'Rotterdam Brass', 'sub' => 0, 'status' => ArtistEngagementStatus::Confirmed],
|
||||
['artist' => 'Rotterdam Brass', 'sub' => 2, 'status' => ArtistEngagementStatus::Requested],
|
||||
['artist' => 'Nachtwacht DJs', 'sub' => 1, 'status' => ArtistEngagementStatus::Option],
|
||||
['artist' => 'Nachtwacht DJs', 'sub' => 2, 'status' => ArtistEngagementStatus::Cancelled],
|
||||
['artist' => 'De Lichtbrigade', 'sub' => 0, 'status' => ArtistEngagementStatus::Draft],
|
||||
['artist' => 'De Lichtbrigade', 'sub' => 1, 'status' => ArtistEngagementStatus::Requested],
|
||||
['artist' => 'De Lichtbrigade', 'sub' => 2, 'status' => ArtistEngagementStatus::Option],
|
||||
$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',
|
||||
];
|
||||
|
||||
$engagements = [];
|
||||
foreach ($statusPlan as $idx => $plan) {
|
||||
/** @var Event $subEvent */
|
||||
$subEvent = $subEvents[$plan['sub']];
|
||||
$artist = $artists[$plan['artist']];
|
||||
|
||||
$attrs = [
|
||||
'artist_id' => $artist->id,
|
||||
'event_id' => $subEvent->id,
|
||||
'booking_status' => $plan['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' => 2,
|
||||
'guests_count' => 4,
|
||||
'advancing_completed_count' => 0,
|
||||
'advancing_total_count' => 0,
|
||||
];
|
||||
|
||||
if ($plan['status'] === ArtistEngagementStatus::Option) {
|
||||
$attrs['option_expires_at'] = CarbonImmutable::now()->addDays(14);
|
||||
$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;
|
||||
}
|
||||
if ($plan['status'] === ArtistEngagementStatus::Requested) {
|
||||
$attrs['requested_at'] = CarbonImmutable::now()->subDays(3);
|
||||
$i++;
|
||||
// Safety: avoid infinite loop if combinations exhaust
|
||||
if ($i > count($cores) * count($suffixes) * 2) {
|
||||
$names[] = $core.' '.$suffix.' '.Str::upper(Str::random(3));
|
||||
}
|
||||
if ($plan['status'] === ArtistEngagementStatus::Contracted) {
|
||||
$attrs['fee_amount'] = 7500.00;
|
||||
}
|
||||
|
||||
$engagements[$idx] = ArtistEngagement::create($attrs);
|
||||
}
|
||||
|
||||
// Performances (13) — most engagements get 1 perf, a couple
|
||||
// get 2 (D17). One parked. One B2B pair on Mainstage Saturday.
|
||||
$perfPlan = [
|
||||
// [engagementIdx, stageName|null, subIdx, hour, minute, duration]
|
||||
[0, 'Mainstage', 0, 22, 0, 75], // Donker & Licht — Vrijdag mainstage
|
||||
[1, 'Mainstage', 1, 23, 30, 60], // Donker & Licht — Zaterdag mainstage (B2B partner-A)
|
||||
[1, 'Mainstage', 1, 21, 0, 60], // …extra perf same engagement (D17 multi-perf)
|
||||
[2, 'Havana', 0, 21, 0, 60],
|
||||
[3, 'Havana', 1, 22, 30, 60],
|
||||
[4, 'Stairway', 1, 21, 0, 75],
|
||||
[5, 'Socialite', 0, 19, 0, 60],
|
||||
[6, 'Socialite', 2, 17, 0, 45],
|
||||
[7, 'Mainstage', 1, 23, 33, 60], // B2B partner-B (3-min offset → seeds B2B detector)
|
||||
[9, null, 0, 0, 0, 60], // De Lichtbrigade Vrijdag = parked / wachtrij
|
||||
[10, 'Stairway', 1, 19, 0, 60],
|
||||
[11, 'Havana', 2, 20, 30, 60],
|
||||
[4, 'Stairway', 1, 22, 30, 60], // multi-perf on same engagement (D17)
|
||||
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<array{name: string, color: string, capacity: int, sort_order: int}> $specs
|
||||
* @param array<int, Event> $performanceDays
|
||||
* @return Collection<string, Stage>
|
||||
*/
|
||||
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,
|
||||
];
|
||||
|
||||
foreach ($perfPlan as $row) {
|
||||
[$engagementIdx, $stageName, $subIdx, $hour, $minute, $minutes] = $row;
|
||||
$engagement = $engagements[$engagementIdx];
|
||||
/** @var Event $subEvent */
|
||||
$subEvent = $subEvents[$subIdx];
|
||||
|
||||
$start = CarbonImmutable::parse($subEvent->start_date)->setTime($hour, $minute);
|
||||
|
||||
Performance::create([
|
||||
'engagement_id' => $engagement->id,
|
||||
'event_id' => $subEvent->id,
|
||||
'stage_id' => $stageName === null ? null : $stages[$stageName]->id,
|
||||
'lane' => 0,
|
||||
'start_at' => $start,
|
||||
'end_at' => $start->addMinutes($minutes),
|
||||
'version' => 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ class DevSeeder extends Seeder
|
||||
/** @var array<string, \App\Models\PersonTag> */
|
||||
private array $personTags = [];
|
||||
|
||||
/** @var array{genres: \Illuminate\Support\Collection<string, \App\Models\Genre>, artists: \Illuminate\Support\Collection<int, \App\Models\Artist>}|null */
|
||||
private ?array $artistPool = null;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(RoleSeeder::class);
|
||||
@@ -163,7 +166,14 @@ class DevSeeder extends Seeder
|
||||
|
||||
$templatesCreated = FormBuilderDevSeeder::seedSystemTemplates($this->org);
|
||||
|
||||
$this->command->info(" Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags, {$templatesCreated} form_templates created");
|
||||
// ── Org-wide artist pool (8 genres + 125 artists) ──
|
||||
// Used as the shared roster: every event draws scheduled +
|
||||
// unscheduled engagements from this pool.
|
||||
$this->artistPool = ArtistTimetableDevSeeder::seedOrganisationPool($this->org, 125);
|
||||
$artistCount = $this->artistPool['artists']->count();
|
||||
$genreCount = $this->artistPool['genres']->count();
|
||||
|
||||
$this->command->info(" Organisation, 8 users, 6 companies, 7 crowd types, 10 person tags, {$templatesCreated} form_templates, {$genreCount} genres, {$artistCount} artists created");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -956,14 +966,16 @@ class DevSeeder extends Seeder
|
||||
FormBuilderDevSeeder::seedEventRegistrationShowcase($this->org, $festival, $this->command);
|
||||
}
|
||||
|
||||
// RFC-TIMETABLE v0.2 — artist + timetable fixture for the
|
||||
// festival (4 stages, 6 artists, 12 engagements, 13 perfs).
|
||||
ArtistTimetableDevSeeder::seedForFestival(
|
||||
// RFC-TIMETABLE v0.2 — festival timetable: 5 stages, ~30 scheduled
|
||||
// performances over 3 days + ~12 unscheduled engagements (linked
|
||||
// but not yet slotted on a stage/timeslot). Draws from the org pool.
|
||||
$tt = ArtistTimetableDevSeeder::seedForFestival(
|
||||
$this->org,
|
||||
$festival,
|
||||
[$vrijdag, $zaterdag, $zondag],
|
||||
$this->artistPool,
|
||||
);
|
||||
$this->command->info(' Artist timetable: 4 stages, 12 stage_days, 6 artists, 12 engagements, 13 performances');
|
||||
$this->command->info(" Artist timetable: {$tt['stages']} stages, {$tt['engagements']} engagements ({$tt['unscheduled']} unscheduled), {$tt['performances']} performances");
|
||||
|
||||
$this->command->info(' Echt Feesten 2026 complete');
|
||||
});
|
||||
@@ -1031,6 +1043,20 @@ class DevSeeder extends Seeder
|
||||
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($braderie, $formSchema);
|
||||
$this->command->info(" Form schema + 16 fields + {$submissions} submissions created");
|
||||
|
||||
// Stages + timetable (flat event, 1 day, lighter program)
|
||||
$tt = ArtistTimetableDevSeeder::seedForFlatEvent($this->org, $braderie, $this->artistPool, [
|
||||
'stages' => [
|
||||
['name' => 'Marktplein Podium', 'color' => '#e85d75', 'capacity' => 800, 'sort_order' => 1],
|
||||
['name' => 'Akoestiekhoek', 'color' => '#22c55e', 'capacity' => 200, 'sort_order' => 2],
|
||||
],
|
||||
'scheduled' => 8,
|
||||
'unscheduled' => 4,
|
||||
'pool_offset' => 50,
|
||||
'start_hour' => 11,
|
||||
'end_hour' => 18,
|
||||
]);
|
||||
$this->command->info(" Artist timetable: {$tt['stages']} stages, {$tt['engagements']} engagements ({$tt['unscheduled']} unscheduled), {$tt['performances']} performances");
|
||||
|
||||
$this->command->info(' Braderie Dorpstown 2026 complete');
|
||||
});
|
||||
}
|
||||
@@ -1206,6 +1232,18 @@ class DevSeeder extends Seeder
|
||||
$formSchema = FormBuilderDevSeeder::seedEventSchema($ijsbaan);
|
||||
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($ijsbaan, $formSchema);
|
||||
$this->command->info(" Form schema + 16 fields + {$submissions} submissions created");
|
||||
|
||||
// Stages + timetable (series, 4 weekend editions)
|
||||
$tt = ArtistTimetableDevSeeder::seedForSeries($this->org, $ijsbaan, $weeks, $this->artistPool, [
|
||||
'stages' => [
|
||||
['name' => 'Schaatspaviljoen', 'color' => '#0ea5e9', 'capacity' => 800, 'sort_order' => 1],
|
||||
['name' => 'Verwarmde Tent', 'color' => '#22c55e', 'capacity' => 300, 'sort_order' => 2],
|
||||
],
|
||||
'per_week_scheduled' => 3,
|
||||
'per_week_unscheduled' => 2,
|
||||
'pool_offset' => 65,
|
||||
]);
|
||||
$this->command->info(" Artist timetable: {$tt['stages']} stages, {$tt['engagements']} engagements ({$tt['unscheduled']} unscheduled), {$tt['performances']} performances");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1378,6 +1416,24 @@ class DevSeeder extends Seeder
|
||||
$formSchema = FormBuilderDevSeeder::seedEventSchema($koningsdag);
|
||||
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($koningsdag, $formSchema);
|
||||
$this->command->info(" Form schema + 16 fields + {$submissions} submissions created");
|
||||
|
||||
// Stages + timetable (closed event — most acts contracted+done,
|
||||
// but a small backlog of unscheduled engagements lives in the
|
||||
// pool to demo "linked, not slotted" alongside historical data.)
|
||||
$tt = ArtistTimetableDevSeeder::seedForFlatEvent($this->org, $koningsdag, $this->artistPool, [
|
||||
'stages' => [
|
||||
['name' => 'Erasmusbrug Podium', 'color' => '#e85d75', 'capacity' => 2000, 'sort_order' => 1],
|
||||
['name' => 'Willemsplein Stage', 'color' => '#0ea5e9', 'capacity' => 1500, 'sort_order' => 2],
|
||||
['name' => 'Oude Haven Floater', 'color' => '#7c3aed', 'capacity' => 600, 'sort_order' => 3],
|
||||
],
|
||||
'scheduled' => 12,
|
||||
'unscheduled' => 4,
|
||||
'pool_offset' => 90,
|
||||
'start_hour' => 12,
|
||||
'end_hour' => 23,
|
||||
'force_status' => \App\Enums\Artist\ArtistEngagementStatus::Contracted,
|
||||
]);
|
||||
$this->command->info(" Artist timetable: {$tt['stages']} stages, {$tt['engagements']} engagements ({$tt['unscheduled']} unscheduled), {$tt['performances']} performances");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1410,6 +1466,21 @@ class DevSeeder extends Seeder
|
||||
|
||||
FormBuilderDevSeeder::seedEventSchema($event);
|
||||
|
||||
// Stages + minimal timetable (draft event — 4 acts in option/draft
|
||||
// status with a couple already slotted, plus a few unscheduled).
|
||||
$tt = ArtistTimetableDevSeeder::seedForFlatEvent($this->org, $event, $this->artistPool, [
|
||||
'stages' => [
|
||||
['name' => 'Deliplein', 'color' => '#7c3aed', 'capacity' => 500, 'sort_order' => 1],
|
||||
['name' => 'Kaapse Bar', 'color' => '#22c55e', 'capacity' => 200, 'sort_order' => 2],
|
||||
],
|
||||
'scheduled' => 4,
|
||||
'unscheduled' => 5,
|
||||
'pool_offset' => 110,
|
||||
'start_hour' => 21,
|
||||
'end_hour' => 26,
|
||||
]);
|
||||
$this->command->info(" Draft event: {$tt['stages']} stages, {$tt['engagements']} engagements ({$tt['unscheduled']} unscheduled), {$tt['performances']} performances");
|
||||
|
||||
$this->command->info(' Empty draft event created (with form schema, 0 submissions)');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user