Files
crewli/api/database/seeders/ArtistTimetableDevSeeder.php
bert.hausmans 006755ac1b fix(seeder): align ArtistTimetableDevSeeder to canonical engagement-vs-performance model (B1)
Phase A diagnosed an empty SPA timetable as a controller filter bug. B1.1's
schema-verify gate proved the opposite: the seeder violates Model A, the
controllers are correct.

Canonical model (Model A) per:
  - dev-docs/SCHEMA.md:1285  artist_engagements.event_id → festival OR flat event
  - dev-docs/SCHEMA.md:1329  performances.event_id      → sub-event OR flat event ("show host")
  - dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md:1247-1257 (§10.2 contract)
    "performance.event_id must be flat event OR a sub-event of the
     engagement.event_id festival"
  - dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md:455-477 (§D17)
    "Friday + Saturday under one combined deal = 1 engagement, 2 performances"
    — only works if engagement is at festival level

Controller audit (B1.2): all five filters in
api/app/Http/Controllers/Api/V1/Artist/{PerformanceController,
ArtistEngagementController, StageController}.php already match Model A.
No controller changes needed.

Seeder change (B1.3) — single consistent fix:

ArtistTimetableDevSeeder::seedForFestival now creates one engagement per
(artist, festival) instead of per (artist, sub-event). When the same artist
recurs across iterations on different sub-events, the existing engagement
is reused and another performance is added (the D17 multi-perf path).
Performances continue to carry event_id = sub-event.

Same model fix in seedForSeries (engagement at parent series, performance
at week sub-event).

seedForFlatEvent already conformed (engagement.event_id = performance.event_id
= the flat event itself).

Existence-check semantics shift from `where event_id = $subEvent->id` to
`where event_id = $festival->id` (or $parent->id for series). Numerically
the test counts hold because the bucket-cycling makes scheduled artists
distinct within the festival window.

Tests (B1.4) — new TimetableSeederControllerIntegrationTest with 7 assertions:
  - engagement.event_id is at festival level (DB invariant)
  - performance.event_id is at sub-event level (DB invariant)
  - GET /performances?day={subEvent} returns non-empty + correct event_ids
  - GET /performances unfiltered returns all sub-event performances
  - GET /performances?stage_id=null returns the seeded parked perf
  - GET /engagements returns engagements with event_id = festival
  - GET /stages returns 5 stages with event_id = festival

This locks the visible-symptom regression from Session 4: an empty SPA
timetable on a freshly-seeded festival cannot land again silently.

Existing ArtistTimetableDevSeederTest (4 tests) and the broader Artist
suite (121 tests) all stay green. composer analyse + Pint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:11 +02:00

585 lines
25 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Enums\Artist\ArtistEngagementStatus;
use App\Models\Artist;
use App\Models\ArtistContact;
use App\Models\ArtistEngagement;
use App\Models\Event;
use App\Models\Genre;
use App\Models\Organisation;
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 org-wide artist roster + per-event timetable fixtures.
*
* 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)
*
* Every event seeded gets:
* - 25 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
{
/**
* 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 ['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];
// Per RFC §D10/§D17 + SCHEMA.md:1285+1329:
// engagement.event_id = festival (the parent — one per artist per festival)
// performance.event_id = sub-event ("show host" day)
// Same artist on multiple sub-events shares ONE engagement (D17:
// "Friday + Saturday under one combined deal = 1 engagement, 2 performances").
foreach ($subEvents as $dayIdx => $subEvent) {
// Each sub-event gets 10 scheduled performances 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) at the festival — reuse the engagement
// if the artist was already booked for this festival, adding another
// performance under it (D17 multi-perf path).
$engagement = ArtistEngagement::query()
->where('artist_id', $artist->id)
->where('event_id', $festival->id)
->first();
if ($engagement === null) {
$engagement = self::createEngagement($artist, $festival, $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()];
// Skip if this artist already has an engagement at the festival
// (e.g. via the scheduled loop above).
$existing = ArtistEngagement::query()
->where('artist_id', $artist->id)
->where('event_id', $festival->id)
->exists();
if ($existing) {
continue;
}
self::createEngagement($artist, $festival, $unscheduledStatusMix[$u++ % count($unscheduledStatusMix)]);
$engagements++;
$unscheduled++;
}
// ── One parked performance (= scheduled but stage_id = null) ──
// Exercises the Session 4 "wachtrij" UI. Engagement at festival level;
// performance.event_id is the sub-event the parked slot belongs to.
$parkedArtist = $bucket[$bucketIdx++ % $bucket->count()];
$parkedSub = $subEvents[0];
if (! ArtistEngagement::query()->where('artist_id', $parkedArtist->id)->where('event_id', $festival->id)->exists()) {
$parkedEngagement = self::createEngagement($parkedArtist, $festival, 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 with consecutive performances on the same stage 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', $festival->id)->exists()) {
continue;
}
$eng = self::createEngagement($pair['artist'], $festival, 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;
// Same model as the festival path: engagement at the parent series
// event, performance at the per-week sub-event. A resident artist
// playing multiple weeks gets ONE engagement with N performances.
foreach ($subEvents as $weekIdx => $week) {
for ($i = 0; $i < $perWeekScheduled; $i++) {
$artist = $bucket[$bucketIdx++ % $bucket->count()];
$status = [ArtistEngagementStatus::Contracted, ArtistEngagementStatus::Confirmed, ArtistEngagementStatus::Option][$i % 3];
$engagement = ArtistEngagement::query()
->where('artist_id', $artist->id)
->where('event_id', $parent->id)
->first();
if ($engagement === null) {
$engagement = self::createEngagement($artist, $parent, $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', $parent->id)->exists()) {
continue;
}
$status = [ArtistEngagementStatus::Draft, ArtistEngagementStatus::Requested, ArtistEngagementStatus::Option][$i % 3];
self::createEngagement($artist, $parent, $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,
]);
}
}
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',
];
$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<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,
];
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,
]);
}
}