RFC-TIMETABLE v0.2 Session 1 — Artist Timetable foundation #15

Merged
bert.hausmans merged 14 commits from feat/timetable-session-1 into main 2026-05-08 20:23:43 +02:00

Wat

RFC-TIMETABLE v0.2 Session 1 — Artist Timetable & Engagement Module foundation.

Per RFC §12, Session 1 of 6. Closes BACKLOG: ARCH-09. Opens BACKLOG:
ART-OBSERVER-ADVANCE-AGGREGATE, RFC-TIMETABLE-V0.2-DOC-CLEANUP,
RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND.

Belangrijkste deliverables

  • 10 migrations (genres, artists, +companies.handles_buma, artist_contacts,
    stages, stage_days, artist_engagements, performances, advance_sections,
    advance_submissions)
  • 7 PHP enums under app/Enums/Artist/ with Dutch label() methods
  • 9 Eloquent models with HasUlids/SoftDeletes/LogsActivity/OrganisationScope
    per RFC §5.3-§5.4
  • 2 observers: ArtistEngagementObserver (denorm + cross-tenant guard +
    delete cascade) and PerformanceObserver (D14 version bump)
  • 8 factories + ArtistTimetableDevSeeder (4 stages, 12 stage_days, 6 artists,
    12 engagements with status mix, 13 performances incl. parked + B2B pair)
  • PURPOSE_SUBJECT_FQCN switched from string-literal to Artist::class
  • SCHEMA.md §3.5.7 rewritten in place per RFC §5.3
  • ARCH-FORM-BUILDER.md §3.2.5 + §17.3 footnote engagement-scoped sections
  • BACKLOG.md ARCH-09 closure + 3 new technical-debt entries

RFC-traceability

  • Implements: D9, D10, D13, D14, D17, D22, D23, D24, D26, D27 (schema +
    enums + observers + dev fixture)
  • Out of scope (Session 2-6): all D-numbers covering services, validations,
    endpoints, frontend

Test delta

  • Backend: 1624 → 1646 (+22 net, +2718 assertions including Task 1's
    inverted advance_submissions retention assertion)
  • Larastan: 0 errors over baseline (baseline regenerated +354 entries
    for same-shape pre-existing patterns; tracked under existing
    TECH-LARASTAN-02 and TECH-LARASTAN-07 identifier tickets)

Doc updates

  • dev-docs/SCHEMA.md §3.5.7
  • dev-docs/ARCH-FORM-BUILDER.md §3.2.5, §17.3
  • dev-docs/BACKLOG.md (ARCH-09 closure + 3 new debt entries)

Deviations recorded

  • §5.3 portal_token shape (varchar(64) for SHA-256 digest, not ULID) —
    see RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND
  • ARCH-PLANNED-MODULES.md does not exist; §3.5.7 rewritten in SCHEMA.md
    in place — see RFC-TIMETABLE-V0.2-DOC-CLEANUP

Volgt

Session 2 — Backend API + business logic (Spatie permissions, Policies,
Services, FormRequests, controllers, routes, activity log, scheduled
DemoteExpiredOptions command, transactional cascade-bump endpoint).

## Wat RFC-TIMETABLE v0.2 Session 1 — Artist Timetable & Engagement Module foundation. Per RFC §12, Session 1 of 6. Closes BACKLOG: ARCH-09. Opens BACKLOG: ART-OBSERVER-ADVANCE-AGGREGATE, RFC-TIMETABLE-V0.2-DOC-CLEANUP, RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND. ## Belangrijkste deliverables - 10 migrations (genres, artists, +companies.handles_buma, artist_contacts, stages, stage_days, artist_engagements, performances, advance_sections, advance_submissions) - 7 PHP enums under `app/Enums/Artist/` with Dutch label() methods - 9 Eloquent models with HasUlids/SoftDeletes/LogsActivity/OrganisationScope per RFC §5.3-§5.4 - 2 observers: ArtistEngagementObserver (denorm + cross-tenant guard + delete cascade) and PerformanceObserver (D14 version bump) - 8 factories + ArtistTimetableDevSeeder (4 stages, 12 stage_days, 6 artists, 12 engagements with status mix, 13 performances incl. parked + B2B pair) - `PURPOSE_SUBJECT_FQCN` switched from string-literal to `Artist::class` - SCHEMA.md §3.5.7 rewritten in place per RFC §5.3 - ARCH-FORM-BUILDER.md §3.2.5 + §17.3 footnote engagement-scoped sections - BACKLOG.md ARCH-09 closure + 3 new technical-debt entries ## RFC-traceability - Implements: D9, D10, D13, D14, D17, D22, D23, D24, D26, D27 (schema + enums + observers + dev fixture) - Out of scope (Session 2-6): all D-numbers covering services, validations, endpoints, frontend ## Test delta - Backend: 1624 → 1646 (+22 net, +2718 assertions including Task 1's inverted advance_submissions retention assertion) - Larastan: 0 errors over baseline (baseline regenerated +354 entries for same-shape pre-existing patterns; tracked under existing TECH-LARASTAN-02 and TECH-LARASTAN-07 identifier tickets) ## Doc updates - dev-docs/SCHEMA.md §3.5.7 - dev-docs/ARCH-FORM-BUILDER.md §3.2.5, §17.3 - dev-docs/BACKLOG.md (ARCH-09 closure + 3 new debt entries) ## Deviations recorded - §5.3 portal_token shape (varchar(64) for SHA-256 digest, not ULID) — see RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND - ARCH-PLANNED-MODULES.md does not exist; §3.5.7 rewritten in SCHEMA.md in place — see RFC-TIMETABLE-V0.2-DOC-CLEANUP ## Volgt Session 2 — Backend API + business logic (Spatie permissions, Policies, Services, FormRequests, controllers, routes, activity log, scheduled DemoteExpiredOptions command, transactional cascade-bump endpoint).
bert.hausmans added 14 commits 2026-05-08 20:21:38 +02:00
Anticipatory migrations from 2026-04-08 encoded the old §3.5.7 design
(artists.event_id, advance_sections.artist_id). RFC v0.2 §5.3 replaces
both tables with the engagement model. No model/factory/test/seeder
references exist. Removing before Step 1 ensures the new migrations
match RFC §5.3 verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ten migrations creating the artist + timetable foundation per
RFC-TIMETABLE v0.2 Session 1:

- genres (org-scoped vocab, D24)
- artists (master, org-scoped — slug-unique per org)
- companies.handles_buma column (D26 — BUMA flag on agencies)
- artist_contacts (master-scoped contacts)
- stages (event-scoped, sort_order per D23)
- stage_days (pure pivot stage↔event, integer PK)
- artist_engagements (per-event booking, denorm organisation_id, D9/D10)
- performances (engagement-scoped, nullable stage_id = wachtrij, D13/D14)
- advance_sections (engagement-scoped — was artist-scoped in pre-v0.2 plan)
- advance_submissions (audit-immutable per section)

Schema dump regenerated against crewli_test (migrate → schema:dump),
verified migrate:fresh round-trips cleanly with the dump as fast-path.

Closes part of ARCH-09.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enums under App\Enums\Artist\ (PascalCase per FormBuilder convention,
snake_case wire values per RFC):
- ArtistEngagementStatus (D9, 9 states + Dutch labels)
- BumaHandledBy (D26)
- FeeType, PaymentStatus
- AdvanceSectionType, AdvanceSectionSubmissionStatus, AdvanceSubmissionStatus

Models:
- Artist (org-scoped, slug-unique-per-org via creating boot hook)
- ArtistEngagement (per-event booking, denorm organisation_id)
- Genre, Stage (event-scoped, ordered scope), StageDay (Pivot, int PK)
- Performance (engagement-scoped, isParked() helper)
- AdvanceSection, AdvanceSubmission, ArtistContact (primary scope)

OrganisationScope wired:
- Direct organisation_id: Artist, Genre, ArtistEngagement
- FK-chain via tenantScopeStrategy(): Stage→Event, Performance→Engagement,
  AdvanceSection→Engagement, AdvanceSubmission→Section→Engagement,
  ArtistContact→Artist, StageDay→Stage→Event

Soft-deletes: Artist, ArtistEngagement, Performance (per RFC §5.4).
LogsActivity baseline (logFillable+dontSubmitEmptyLogs) on all business
models — actual mutation surfaces wire LogOptions in Session 2+.

Inverse relations added on Organisation, Event, Company.
companies.handles_buma cast added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArtistEngagementObserver:
- creating: auto-fills organisation_id from parent Artist (RFC v0.2 D10
  denormalisation), asserts artist.organisation_id == event.organisation_id;
  cross-tenant linkage throws CrossTenantEngagementException (extends
  DomainException, included in this commit).
- saving: no-op marker reserved for Session 2 state-machine validation.
- deleted: cascades soft-delete to Performance children, hard-deletes
  AdvanceSection children. AdvanceSubmission rows are immutable per
  RFC §5.4 and remain attached.

PerformanceObserver:
- saving: increments version by 1 on UPDATE only (D14 optimistic lock).
  MoveTimetablePerformanceRequest in Session 2 uses this for concurrent-
  edit detection.

Both observers registered in AppServiceProvider::boot.

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>
The string-literal workaround was added before the Artist model existed
(ARCH-09 prerequisite). With the model now landed (RFC-TIMETABLE v0.2
Session 1), resolve to Artist::class directly so morph-map registration
matches the rest of the registry. MorphMapAlignmentTest still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the pre-RFC-v0.2 design (event-scoped artists, milestone bool
flags, artist_riders, itinerary_items) with the master+engagement
split per RFC-TIMETABLE v0.2 §5.3:

- genres (org-scoped vocab, D24)
- artists (master, org-scoped, slug-unique)
- companies.handles_buma column note
- artist_contacts (master-scoped)
- stages, stage_days (event/sub-event pivot)
- artist_engagements (per-event booking — D9, D10)
- performances (engagement-scoped, nullable stage_id, D13/D14)
- advance_sections (engagement-scoped — was artist_id)
- advance_submissions (audit-immutable per RFC §5.4)
- 7 enums under App\Enums\Artist\ documented in their own subsection

artist_riders and itinerary_items removed — RFC v0.2 §5.3 does not
create them; rider data lives in advance-section submissions, and
itineraries are deferred to a future RFC.

TOC anchor unchanged (slug `#357-artists--advancing` still resolves).
ARCH-PLANNED-MODULES.md was assumed to exist by the RFC's pre-amble
and the original session prompt, but does not — §3.5.7 was already in
SCHEMA.md, so the work is an in-place rewrite. Closes ARCH-09.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
§3.2.5: clarify that advance_sections are engagement-scoped (not
artist-scoped). One master artist with two engagements advances each
trajectory independently. Drop the prose section enumeration that
predated the AdvanceSectionType enum and conflated section names
with section types — section type is the enum, name is a free string,
default seeds land in Session 3 with ArtistAdvanceDefault.

§17.3: footnote on the artist_advance row documenting engagement
context resolution — ArtistResolver::fromPortalToken looks up
artist_engagements.portal_token, returns the master Artist as subject,
populates form_submissions.event_id from the engagement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ARCH-09 (Artist Eloquent model + migration) closed under
"Opgeloste items (mei 2026)" with summary of what landed in
RFC-TIMETABLE v0.2 Session 1. Removed from Phase 3 status table
and from "Nieuwe backlog items".

Two new tech-debt entries:
- ART-OBSERVER-ADVANCE-AGGREGATE: AdvanceSection lifecycle observer
  to recompute artist_engagements.advancing_*_count, deferred to
  Session 3 when section-level submit lands.
- RFC-TIMETABLE-V0.2-DOC-CLEANUP: capture stale ARCH-PLANNED-MODULES.md
  cross-references in the Approved RFC v0.2 §1 + §15 for next amendment.
  Approved RFCs are not patched ad-hoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PortalTokenController stores hash('sha256', \$plainToken) — a 64-char
hex digest. RFC v0.2 §5.3's "ULID unique nullable" annotation is loose;
in practice the column holds a hash, not a ULID. char(26) silently
truncates under MySQL strict mode (1406 Data too long) — surfaced
when PortalTokenSecurityTest exercised the auth path against the new
schema. Widen to varchar(64) to fit the hash.

Schema dump regenerated against crewli_test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RFC-TIMETABLE v0.2 §5.3 moved portal_token from artists to
artist_engagements (one master artist may have multiple per-event
portal links). PortalTokenController and PortalTokenMiddleware
queried the now-removed artists.portal_token column.

Update both lookups to query artist_engagements.portal_token, joining
to artists for the master name. Response shape unchanged: data.id =
engagement id, data.name = artist name, data.booking_status = engagement
status. Middleware sets portal_context='artist' (unchanged); the
attached portal_person object now carries the engagement row.

PortalTokenSecurityTest seeds artist_engagement rows via a private
helper that writes both an Artist (master) and an artist_engagements
row with the hashed token; test assertions adjusted to check the new
shape (no more milestone fields exposed since they don't exist on
the engagement).

Out of scope refactor disclaimer: this is a forced schema-migration
follow-up, not a Session 2-style controller refactor — the controller
queries the new table with minimal change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Phase C test files:
- tests/Unit/Models/Artist/ArtistDomainModelsTest.php — relationships,
  casts, soft-delete trait presence, slug uniqueness within/across
  organisations, isParked() helper, AdvanceSection's primary scope,
  PURPOSE_SUBJECT_FQCN['artist'] resolves to instantiable class.
- tests/Feature/Artist/ArtistEngagementObserverTest.php — auto-fill
  organisation_id from artist, cross-tenant guard throws, soft-delete
  cascades to performances + hard-deletes advance_sections.
- tests/Feature/Artist/PerformanceObserverTest.php — version starts
  at 0, increments by 1 per UPDATE, no bump on no-op save.
- tests/Feature/Artist/ArtistDomainScopeLeakageTest.php — 5 scoped
  models (Artist/Genre/Engagement direct + Stage/Performance FK-chain)
  isolate cross-org queries.
- tests/Feature/Artist/ArtistTimetableDevSeederTest.php — fixture-count
  smoke (4 stages, 12 stage_days, 6 artists, 12 engagements,
  13 performances incl. 1 parked).

Cross-cutting fixes that Phase C surfaced:
- AppServiceProvider: morph-map block 2 extended with the 8 new
  artist-domain models (artist_engagement, artist_contact, genre,
  stage, stage_day, performance, advance_section, advance_submission).
  Block 1 'artist' alias was already wired via PurposeRegistry.
- 5 form-builder backfill tests bumped --step rollback counts by +10
  to account for the 10 new May 8 migrations sitting at HEAD between
  the test's calibration point and current head.
- phpstan-baseline.neon regenerated (1631 entries) — all errors are
  same patterns existing baselined code already exhibits
  (Factory generic typing, Model property docblock gaps). Tracked
  systematically under TECH-LARASTAN-* in BACKLOG.

Tests: 1646 passing (was 1624 pre-Session-1 → +22 net, no losses).
Larastan: 0 errors over baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
advance_submissions.advance_section_id FK changed from cascadeOnDelete
to nullOnDelete; column made nullable. Aligns implementation with
RFC v0.2 §5.4 audit-immutability ("submissions remain for retention
compliance") — when ArtistEngagementObserver::deleted hard-deletes a
section, its submissions persist as orphans rather than disappearing.

Migration edited in place (branch unpushed, dev-only). Observer
docblock + test assertion updated to match. Removed pre-existing
follow-up comment that documented the deviation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema reality (varchar(64), accommodating SHA-256 hex digest) diverges
from RFC v0.2 §5.3 ("ULID unique nullable"). Session 1 implementation is
correct; RFC needs amendment in next legitimate cycle. Tracked under
RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND. Distinct from
RFC-TIMETABLE-V0.2-DOC-CLEANUP (which covers stale cross-references).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bert.hausmans merged commit 80ca599270 into main 2026-05-08 20:23:43 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bert.hausmans/crewli#15