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\Stage;
|
||||||
use App\Models\StageDay;
|
use App\Models\StageDay;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeds the Artist & Timetable fixture for an existing festival with
|
* Seeds the org-wide artist roster + per-event timetable fixtures.
|
||||||
* three sub-events (e.g. Echt Feesten 2026 → Vrijdag/Zaterdag/Zondag).
|
|
||||||
*
|
*
|
||||||
* Reproduces the prototype audit fixture as closely as possible:
|
* Three entry points:
|
||||||
* - 4 stages on the festival
|
* - seedOrganisationPool() — 8 genres + 125 artists at the organisation level
|
||||||
* - 4 stages × 3 sub-events = 12 stage_days rows
|
* - seedForFestival() — multi-day festival with sub-events (Echt Feesten)
|
||||||
* - 4 genres, 6 master artists with default genre + draw
|
* - seedForFlatEvent() — single-day event (Braderie, Koningsdag, Nacht)
|
||||||
* - 12 engagements with status mix (Draft 1, Requested 2, Option 3,
|
* - seedForSeries() — recurring series with weekly sub-events (IJsbaan)
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
* advance_sections seeding lands in Session 3 with the form-builder
|
* Every event seeded gets:
|
||||||
* integration.
|
* - 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
|
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) {
|
if (count($subEvents) < 3) {
|
||||||
return;
|
return ['stages' => 0, 'engagements' => 0, 'performances' => 0, 'unscheduled' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genres
|
$stages = self::createStages($festival, [
|
||||||
$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([
|
|
||||||
['name' => 'Mainstage', 'color' => '#e85d75', 'capacity' => 4500, 'sort_order' => 1],
|
['name' => 'Mainstage', 'color' => '#e85d75', 'capacity' => 4500, 'sort_order' => 1],
|
||||||
['name' => 'Havana', 'color' => '#0ea5e9', 'capacity' => 1200, 'sort_order' => 2],
|
['name' => 'Havana', 'color' => '#0ea5e9', 'capacity' => 1200, 'sort_order' => 2],
|
||||||
['name' => 'Stairway', 'color' => '#7c3aed', 'capacity' => 800, 'sort_order' => 3],
|
['name' => 'Stairway', 'color' => '#7c3aed', 'capacity' => 800, 'sort_order' => 3],
|
||||||
['name' => 'Socialite', 'color' => '#22c55e', 'capacity' => 600, 'sort_order' => 4],
|
['name' => 'Socialite', 'color' => '#22c55e', 'capacity' => 600, 'sort_order' => 4],
|
||||||
])->mapWithKeys(fn (array $s) => [
|
['name' => 'Tent op het Plein', 'color' => '#f59e0b', 'capacity' => 400, 'sort_order' => 5],
|
||||||
$s['name'] => Stage::create(['event_id' => $festival->id, ...$s]),
|
], $subEvents);
|
||||||
]);
|
|
||||||
|
|
||||||
// stage_days — every stage active every sub-event day
|
// Reserve the first 50 artists from the org pool for festival
|
||||||
foreach ($stages as $stage) {
|
// engagements. (Other events draw from later slices to keep
|
||||||
foreach ($subEvents as $subEvent) {
|
// some artists reserved for "unscheduled" / pure-pool variety.)
|
||||||
StageDay::create([
|
$bucket = $pool['artists']->slice(0, 50)->values();
|
||||||
'stage_id' => $stage->id,
|
$bucketIdx = 0;
|
||||||
'event_id' => $subEvent->id,
|
|
||||||
|
$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
|
return $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],
|
* Generate $count unique artist names by combining curated parts.
|
||||||
['name' => 'Rotterdam Brass', 'genre' => 'Live band', 'draw' => 900, 'star' => 4],
|
* The first ~40 entries are hand-picked "real-feeling" names so the
|
||||||
['name' => 'Nachtwacht DJs', 'genre' => 'Techno', 'draw' => 1100, 'star' => 3],
|
* top of the list looks human; the remainder are generated.
|
||||||
['name' => 'De Lichtbrigade', 'genre' => 'Live band', 'draw' => 500, 'star' => 3],
|
*
|
||||||
|
* @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 = [];
|
$cores = [
|
||||||
foreach ($artistsData as $data) {
|
'Voltage', 'Echo', 'Lumen', 'Donker', 'Licht', 'Storm', 'Zon', 'Maan',
|
||||||
/** @var Artist $artist */
|
'Stadse', 'Polder', 'Noord', 'Zuid', 'Oost', 'West', 'Maas', 'IJssel',
|
||||||
$artist = Artist::create([
|
'Rotterdam', 'Amsterdam', 'Utrecht', 'Eindhoven', 'Brabant', 'Limburg',
|
||||||
'organisation_id' => $org->id,
|
'Kraak', 'Veer', 'Brug', 'Plein', 'Markt', 'Park', 'Tuin', 'Bos',
|
||||||
'name' => $data['name'],
|
'Ster', 'Wolk', 'Regen', 'Wind', 'Vuur', 'IJs', 'Sneeuw', 'Mist',
|
||||||
'default_genre_id' => $genres[$data['genre']]->id,
|
];
|
||||||
'default_draw' => $data['draw'],
|
$suffixes = [
|
||||||
'star_rating' => $data['star'],
|
'Collective', 'Project', 'Sound', 'Live', 'Crew', 'Brothers', 'Sisters',
|
||||||
'home_base_country' => 'NL',
|
'Sessions', 'Orkest', 'Band', 'Trio', 'Quartet', 'DJs', 'System',
|
||||||
]);
|
'Allstars', 'Republic', 'Union', 'Express', 'Riders', 'Beats',
|
||||||
$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],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$engagements = [];
|
$names = $curated;
|
||||||
foreach ($statusPlan as $idx => $plan) {
|
$taken = array_flip($names);
|
||||||
/** @var Event $subEvent */
|
$i = 0;
|
||||||
$subEvent = $subEvents[$plan['sub']];
|
while (count($names) < $count) {
|
||||||
$artist = $artists[$plan['artist']];
|
$core = $cores[$i % count($cores)];
|
||||||
|
$suffix = $suffixes[(int) ($i / count($cores)) % count($suffixes)];
|
||||||
$attrs = [
|
$candidate = $core.' '.$suffix;
|
||||||
'artist_id' => $artist->id,
|
if (! isset($taken[$candidate])) {
|
||||||
'event_id' => $subEvent->id,
|
$names[] = $candidate;
|
||||||
'booking_status' => $plan['status'],
|
$taken[$candidate] = true;
|
||||||
'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);
|
|
||||||
}
|
}
|
||||||
if ($plan['status'] === ArtistEngagementStatus::Requested) {
|
$i++;
|
||||||
$attrs['requested_at'] = CarbonImmutable::now()->subDays(3);
|
// 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
|
return array_slice($names, 0, $count);
|
||||||
// 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
|
* Create stages on $event and stage_days entries linking each stage
|
||||||
[1, 'Mainstage', 1, 23, 30, 60], // Donker & Licht — Zaterdag mainstage (B2B partner-A)
|
* to every $performanceDay (the parent event itself for flat events,
|
||||||
[1, 'Mainstage', 1, 21, 0, 60], // …extra perf same engagement (D17 multi-perf)
|
* each sub-event for festivals/series).
|
||||||
[2, 'Havana', 0, 21, 0, 60],
|
*
|
||||||
[3, 'Havana', 1, 22, 30, 60],
|
* @param list<array{name: string, color: string, capacity: int, sort_order: int}> $specs
|
||||||
[4, 'Stairway', 1, 21, 0, 75],
|
* @param array<int, Event> $performanceDays
|
||||||
[5, 'Socialite', 0, 19, 0, 60],
|
* @return Collection<string, Stage>
|
||||||
[6, 'Socialite', 2, 17, 0, 45],
|
*/
|
||||||
[7, 'Mainstage', 1, 23, 33, 60], // B2B partner-B (3-min offset → seeds B2B detector)
|
private static function createStages(Event $event, array $specs, array $performanceDays): Collection
|
||||||
[9, null, 0, 0, 0, 60], // De Lichtbrigade Vrijdag = parked / wachtrij
|
{
|
||||||
[10, 'Stairway', 1, 19, 0, 60],
|
$stages = collect();
|
||||||
[11, 'Havana', 2, 20, 30, 60],
|
foreach ($specs as $spec) {
|
||||||
[4, 'Stairway', 1, 22, 30, 60], // multi-perf on same engagement (D17)
|
$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) {
|
if ($status === ArtistEngagementStatus::Option) {
|
||||||
[$engagementIdx, $stageName, $subIdx, $hour, $minute, $minutes] = $row;
|
$attrs['option_expires_at'] = CarbonImmutable::now()->addDays(fake()->numberBetween(7, 30));
|
||||||
$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::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> */
|
/** @var array<string, \App\Models\PersonTag> */
|
||||||
private array $personTags = [];
|
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
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call(RoleSeeder::class);
|
$this->call(RoleSeeder::class);
|
||||||
@@ -163,7 +166,14 @@ class DevSeeder extends Seeder
|
|||||||
|
|
||||||
$templatesCreated = FormBuilderDevSeeder::seedSystemTemplates($this->org);
|
$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);
|
FormBuilderDevSeeder::seedEventRegistrationShowcase($this->org, $festival, $this->command);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC-TIMETABLE v0.2 — artist + timetable fixture for the
|
// RFC-TIMETABLE v0.2 — festival timetable: 5 stages, ~30 scheduled
|
||||||
// festival (4 stages, 6 artists, 12 engagements, 13 perfs).
|
// performances over 3 days + ~12 unscheduled engagements (linked
|
||||||
ArtistTimetableDevSeeder::seedForFestival(
|
// but not yet slotted on a stage/timeslot). Draws from the org pool.
|
||||||
|
$tt = ArtistTimetableDevSeeder::seedForFestival(
|
||||||
$this->org,
|
$this->org,
|
||||||
$festival,
|
$festival,
|
||||||
[$vrijdag, $zaterdag, $zondag],
|
[$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');
|
$this->command->info(' Echt Feesten 2026 complete');
|
||||||
});
|
});
|
||||||
@@ -1031,6 +1043,20 @@ class DevSeeder extends Seeder
|
|||||||
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($braderie, $formSchema);
|
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($braderie, $formSchema);
|
||||||
$this->command->info(" Form schema + 16 fields + {$submissions} submissions created");
|
$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');
|
$this->command->info(' Braderie Dorpstown 2026 complete');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1206,6 +1232,18 @@ class DevSeeder extends Seeder
|
|||||||
$formSchema = FormBuilderDevSeeder::seedEventSchema($ijsbaan);
|
$formSchema = FormBuilderDevSeeder::seedEventSchema($ijsbaan);
|
||||||
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($ijsbaan, $formSchema);
|
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($ijsbaan, $formSchema);
|
||||||
$this->command->info(" Form schema + 16 fields + {$submissions} submissions created");
|
$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);
|
$formSchema = FormBuilderDevSeeder::seedEventSchema($koningsdag);
|
||||||
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($koningsdag, $formSchema);
|
$submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($koningsdag, $formSchema);
|
||||||
$this->command->info(" Form schema + 16 fields + {$submissions} submissions created");
|
$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);
|
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)');
|
$this->command->info(' Empty draft event created (with form schema, 0 submissions)');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Feature\Artist;
|
namespace Tests\Feature\Artist;
|
||||||
|
|
||||||
use App\Models\Artist;
|
use App\Models\Artist;
|
||||||
use App\Models\ArtistContact;
|
|
||||||
use App\Models\ArtistEngagement;
|
use App\Models\ArtistEngagement;
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
use App\Models\Genre;
|
use App\Models\Genre;
|
||||||
@@ -22,9 +21,31 @@ final class ArtistTimetableDevSeederTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_seeder_produces_expected_fixture_counts(): void
|
public function test_organisation_pool_creates_125_artists_and_8_genres(): void
|
||||||
{
|
{
|
||||||
$org = Organisation::factory()->create();
|
$org = Organisation::factory()->create();
|
||||||
|
|
||||||
|
$pool = ArtistTimetableDevSeeder::seedOrganisationPool($org, 125);
|
||||||
|
|
||||||
|
$this->assertSame(8, Genre::withoutGlobalScope(OrganisationScope::class)->count());
|
||||||
|
$this->assertSame(125, Artist::withoutGlobalScope(OrganisationScope::class)->count());
|
||||||
|
$this->assertCount(8, $pool['genres']);
|
||||||
|
$this->assertCount(125, $pool['artists']);
|
||||||
|
|
||||||
|
// Every artist belongs to the seeded organisation
|
||||||
|
$this->assertSame(
|
||||||
|
125,
|
||||||
|
Artist::withoutGlobalScope(OrganisationScope::class)
|
||||||
|
->where('organisation_id', $org->id)
|
||||||
|
->count(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_seed_for_festival_produces_stages_engagements_and_unscheduled(): void
|
||||||
|
{
|
||||||
|
$org = Organisation::factory()->create();
|
||||||
|
$pool = ArtistTimetableDevSeeder::seedOrganisationPool($org, 125);
|
||||||
|
|
||||||
/** @var Event $festival */
|
/** @var Event $festival */
|
||||||
$festival = Event::factory()->for($org)->festival()->create([
|
$festival = Event::factory()->for($org)->festival()->create([
|
||||||
'start_date' => '2026-07-10',
|
'start_date' => '2026-07-10',
|
||||||
@@ -43,18 +64,98 @@ final class ArtistTimetableDevSeederTest extends TestCase
|
|||||||
'end_date' => '2026-07-12',
|
'end_date' => '2026-07-12',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ArtistTimetableDevSeeder::seedForFestival($org, $festival, [$vrijdag, $zaterdag, $zondag]);
|
$result = ArtistTimetableDevSeeder::seedForFestival(
|
||||||
|
$org,
|
||||||
|
$festival,
|
||||||
|
[$vrijdag, $zaterdag, $zondag],
|
||||||
|
$pool,
|
||||||
|
);
|
||||||
|
|
||||||
$this->assertSame(4, Genre::withoutGlobalScope(OrganisationScope::class)->count());
|
// 5 stages, each linked to all 3 sub-events via stage_days
|
||||||
$this->assertSame(4, Stage::withoutGlobalScope(OrganisationScope::class)->count());
|
$this->assertSame(5, Stage::withoutGlobalScope(OrganisationScope::class)->count());
|
||||||
$this->assertSame(12, StageDay::withoutGlobalScope(OrganisationScope::class)->count());
|
$this->assertSame(15, StageDay::withoutGlobalScope(OrganisationScope::class)->count());
|
||||||
$this->assertSame(6, Artist::withoutGlobalScope(OrganisationScope::class)->count());
|
|
||||||
$this->assertSame(6, ArtistContact::withoutGlobalScope(OrganisationScope::class)->count());
|
// Engagement / performance counts (deterministic from the loop structure)
|
||||||
$this->assertSame(12, ArtistEngagement::withoutGlobalScope(OrganisationScope::class)->count());
|
$this->assertSame(45, $result['engagements']);
|
||||||
$this->assertSame(13, Performance::withoutGlobalScope(OrganisationScope::class)->count());
|
$this->assertSame(12, $result['unscheduled']);
|
||||||
$this->assertSame(
|
$this->assertGreaterThanOrEqual(35, $result['performances']);
|
||||||
|
|
||||||
|
// At least one parked performance for the wachtrij UI
|
||||||
|
$this->assertGreaterThanOrEqual(
|
||||||
1,
|
1,
|
||||||
Performance::withoutGlobalScope(OrganisationScope::class)->whereNull('stage_id')->count()
|
Performance::withoutGlobalScope(OrganisationScope::class)->whereNull('stage_id')->count(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unscheduled engagements really have no performances
|
||||||
|
$unscheduledQuery = ArtistEngagement::withoutGlobalScope(OrganisationScope::class)
|
||||||
|
->doesntHave('performances');
|
||||||
|
$this->assertSame(12, $unscheduledQuery->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_seed_for_flat_event_creates_stages_and_mixed_engagements(): void
|
||||||
|
{
|
||||||
|
$org = Organisation::factory()->create();
|
||||||
|
$pool = ArtistTimetableDevSeeder::seedOrganisationPool($org, 125);
|
||||||
|
|
||||||
|
/** @var Event $event */
|
||||||
|
$event = Event::factory()->for($org)->create([
|
||||||
|
'event_type' => 'event',
|
||||||
|
'start_date' => '2026-04-27',
|
||||||
|
'end_date' => '2026-04-27',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = ArtistTimetableDevSeeder::seedForFlatEvent($org, $event, $pool, [
|
||||||
|
'scheduled' => 6,
|
||||||
|
'unscheduled' => 3,
|
||||||
|
'pool_offset' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(2, $result['stages']);
|
||||||
|
$this->assertSame(9, $result['engagements']);
|
||||||
|
$this->assertSame(3, $result['unscheduled']);
|
||||||
|
$this->assertSame(6, $result['performances']);
|
||||||
|
|
||||||
|
// Stage days for the flat event (1 day × 2 stages = 2 rows)
|
||||||
|
$this->assertSame(
|
||||||
|
2,
|
||||||
|
StageDay::withoutGlobalScope(OrganisationScope::class)
|
||||||
|
->where('event_id', $event->id)
|
||||||
|
->count(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_seed_for_series_creates_stages_with_stage_days_per_sub_event(): void
|
||||||
|
{
|
||||||
|
$org = Organisation::factory()->create();
|
||||||
|
$pool = ArtistTimetableDevSeeder::seedOrganisationPool($org, 125);
|
||||||
|
|
||||||
|
/** @var Event $parent */
|
||||||
|
$parent = Event::factory()->for($org)->create([
|
||||||
|
'event_type' => 'series',
|
||||||
|
'start_date' => '2026-12-05',
|
||||||
|
'end_date' => '2027-01-25',
|
||||||
|
]);
|
||||||
|
$subs = [];
|
||||||
|
foreach (['2026-12-06', '2026-12-20', '2027-01-04', '2027-01-25'] as $date) {
|
||||||
|
$subs[] = Event::factory()->for($org)->subEvent($parent)->create([
|
||||||
|
'start_date' => $date,
|
||||||
|
'end_date' => $date,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ArtistTimetableDevSeeder::seedForSeries($org, $parent, $subs, $pool, [
|
||||||
|
'per_week_scheduled' => 2,
|
||||||
|
'per_week_unscheduled' => 1,
|
||||||
|
'pool_offset' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2 stages × 4 sub-events = 8 stage_days
|
||||||
|
$this->assertSame(2, $result['stages']);
|
||||||
|
$this->assertSame(8, StageDay::withoutGlobalScope(OrganisationScope::class)->count());
|
||||||
|
|
||||||
|
// 4 weeks × (2 scheduled + 1 unscheduled) = 12 engagements
|
||||||
|
$this->assertSame(12, $result['engagements']);
|
||||||
|
$this->assertSame(4, $result['unscheduled']);
|
||||||
|
$this->assertSame(8, $result['performances']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user