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>
Eight factories with named states (Genre, Artist, ArtistContact, Stage,
ArtistEngagement, Performance, AdvanceSection, AdvanceSubmission).
ArtistTimetableDevSeeder hooked into DevSeeder::seedEchtFeesten after
the form-builder showcase. Produces:
- 4 stages (Mainstage, Havana, Stairway, Socialite) with prototype-style
hex colours
- 4 stages × 3 sub-events = 12 stage_days rows
- 4 genres (Hardstyle, Techno, Indie, Live band)
- 6 master artists, each with one tour-manager ArtistContact
- 12 engagements with status mix (1 Draft, 2 Requested, 3 Option,
2 Confirmed, 3 Contracted, 1 Cancelled). Two artists have two
engagements each (different sub-events) — exercises D17 multi-
engagement-per-artist.
- 13 performances, including one parked (stage_id=null = wachtrij)
and one B2B pair within 3 minutes on Mainstage Saturday to seed
the Session 4 frontend B2B detector.
Also fix LogOptions method name across 8 models: dontSubmitEmptyLogs()
→ dontLogEmptyChanges() (Spatie's actual API; surfaced when DevSeeder
ran).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>