docs(schema): rewrite §3.5.7 Artists & Advancing — RFC v0.2 alignment
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>
This commit is contained in:
@@ -1150,162 +1150,265 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
|
|||||||
|
|
||||||
## 3.5.7 Artists & Advancing
|
## 3.5.7 Artists & Advancing
|
||||||
|
|
||||||
### `artists`
|
> **Authoritative spec:** RFC-TIMETABLE v0.2 §5.3. The pre-v0.2 schema
|
||||||
|
> (event-scoped `artists`, milestone bool flags, `artist_riders`,
|
||||||
|
> `itinerary_items`) was replaced in 2026-05 — see
|
||||||
|
> `dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md` for the design
|
||||||
|
> decisions (D9, D10, D13, D14, D17, D23, D24, D26).
|
||||||
|
|
||||||
| Column | Type | Notes |
|
The artist domain splits into a **master record** (`artists`,
|
||||||
| ------------------------------ | ------------------ | -------------------------------------------------------------- |
|
org-scoped) and a **per-event booking** (`artist_engagements`).
|
||||||
| `id` | ULID | PK |
|
The split allows one artist to have multiple engagements across
|
||||||
| `event_id` | ULID FK | → events |
|
events, each with its own deal, advance trajectory, and timetable
|
||||||
| `name` | string | |
|
performances.
|
||||||
| `booking_status` | enum | `concept\|requested\|option\|confirmed\|contracted\|cancelled` |
|
|
||||||
| `star_rating` | tinyint | 1–5 |
|
|
||||||
| `project_leader_id` | ULID FK nullable | → users |
|
|
||||||
| `milestone_offer_in` | bool | Default: false |
|
|
||||||
| `milestone_offer_agreed` | bool | Default: false |
|
|
||||||
| `milestone_confirmed` | bool | Default: false |
|
|
||||||
| `milestone_announced` | bool | Default: false |
|
|
||||||
| `milestone_schedule_confirmed` | bool | Default: false |
|
|
||||||
| `milestone_itinerary_sent` | bool | Default: false |
|
|
||||||
| `milestone_advance_sent` | bool | Default: false |
|
|
||||||
| `milestone_advance_received` | bool | Default: false |
|
|
||||||
| `advance_open_from` | datetime nullable | |
|
|
||||||
| `advance_open_to` | datetime nullable | |
|
|
||||||
| `show_advance_share_page` | bool | Default: true |
|
|
||||||
| `portal_token` | ULID unique | Access to artist portal without account |
|
|
||||||
| `deleted_at` | timestamp nullable | Soft delete |
|
|
||||||
|
|
||||||
**Relations:** `hasMany` performances, advance_sections, artist_contacts, artist_riders
|
### `genres`
|
||||||
**Soft delete:** yes
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| ----------------- | ------- | -------------------------------- |
|
||||||
|
| `id` | ULID | PK |
|
||||||
|
| `organisation_id` | ULID FK | → organisations |
|
||||||
|
| `name` | string(40) | |
|
||||||
|
| `color` | string(7) nullable | hex |
|
||||||
|
| `sort_order` | int default 0 | |
|
||||||
|
| `is_active` | bool default true | use instead of soft delete |
|
||||||
|
| `created_at`, `updated_at` | timestamps | |
|
||||||
|
|
||||||
|
**Unique:** `UNIQUE(organisation_id, name)`
|
||||||
|
**Soft delete:** no — `is_active=false` is the retire mechanism (D24)
|
||||||
|
**Scope:** `OrganisationScope` (direct `organisation_id`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `performances`
|
### `artists` (master)
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
| ----------------- | ------- | ------------------------------- |
|
| ------------------- | ---------------- | ------------------------------------------- |
|
||||||
| `id` | ULID | PK |
|
| `id` | ULID | PK |
|
||||||
| `artist_id` | ULID FK | → artists |
|
| `organisation_id` | ULID FK | → organisations |
|
||||||
| `stage_id` | ULID FK | → stages |
|
| `name` | string(120) | |
|
||||||
| `date` | date | |
|
| `slug` | string(120) | unique per organisation, generated from name |
|
||||||
| `start_time` | time | |
|
| `default_genre_id` | ULID FK nullable | → genres (D24) |
|
||||||
| `end_time` | time | |
|
| `default_draw` | int nullable | for capacity-warn defaults |
|
||||||
| `booking_status` | string | |
|
| `star_rating` | tinyint nullable | 1–5 |
|
||||||
| `check_in_status` | enum | `expected\|checked_in\|no_show` |
|
| `home_base_country` | string(2) nullable | ISO 3166-1 alpha-2 |
|
||||||
|
| `agent_company_id` | ULID FK nullable | → companies (typically `type=agency`) |
|
||||||
|
| `notes` | text nullable | |
|
||||||
|
| `created_at`, `updated_at`, `deleted_at` | | soft delete |
|
||||||
|
|
||||||
**Indexes:** `(stage_id, date, start_time, end_time)`
|
**Unique:** `UNIQUE(organisation_id, slug)`
|
||||||
|
**Indexes:** `(organisation_id, name)`, `(default_genre_id)`, `(agent_company_id)`
|
||||||
|
**Soft delete:** yes (RFC §5.4)
|
||||||
|
**Scope:** `OrganisationScope` (direct `organisation_id`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `companies.handles_buma` (added column)
|
||||||
|
|
||||||
|
The existing `companies` table (§3.5.5) gains a single bool column:
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| -------------- | ---------------- | -------------------------------------- |
|
||||||
|
| `handles_buma` | bool default false | Whether an agency reports BUMA on the artist's behalf (RFC D26) |
|
||||||
|
|
||||||
|
The existing `type` enum is unchanged — the `agency` value is reused
|
||||||
|
for booking agencies in the artist domain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `artist_contacts` (master-scoped)
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| -------------------- | ---------------- | ------------------------------ |
|
||||||
|
| `id` | ULID | PK |
|
||||||
|
| `artist_id` | ULID FK | → artists (master) |
|
||||||
|
| `name` | string(120) | |
|
||||||
|
| `email` | string nullable | |
|
||||||
|
| `phone` | string nullable | |
|
||||||
|
| `role` | string(60) | tour_manager, agent, manager… |
|
||||||
|
| `is_primary` | bool default false | |
|
||||||
|
| `receives_briefing` | bool default false | |
|
||||||
|
| `receives_infosheet` | bool default false | |
|
||||||
|
| `created_at`, `updated_at` | timestamps | |
|
||||||
|
|
||||||
|
**Indexes:** `(artist_id, role)`
|
||||||
|
**Soft delete:** no
|
||||||
|
**Scope:** `OrganisationScope` (FK-chain via `artist_id → artists.organisation_id`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `stages`
|
### `stages`
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
| ---------- | ------------ | -------- |
|
| ------------ | ------------ | -------------------------------- |
|
||||||
| `id` | ULID | PK |
|
| `id` | ULID | PK |
|
||||||
| `event_id` | ULID FK | → events |
|
| `event_id` | ULID FK | → events (festival or flat event) |
|
||||||
| `name` | string | |
|
| `name` | string(120) | |
|
||||||
| `color` | string | hex |
|
| `color` | string(7) | hex |
|
||||||
| `capacity` | int nullable | |
|
| `capacity` | int nullable | drives capacity-warn (RFC D23) |
|
||||||
|
| `sort_order` | int default 0 | (RFC D23) |
|
||||||
|
| `created_at`, `updated_at` | timestamps | |
|
||||||
|
|
||||||
**Relations:** `hasMany` performances
|
**Unique:** `UNIQUE(event_id, name)`
|
||||||
**Indexes:** `(event_id)`
|
**Indexes:** `(event_id, sort_order)`
|
||||||
|
**Soft delete:** no — destructive deletion is rare; query complexity isn't worth the safety win
|
||||||
|
**Scope:** `OrganisationScope` (FK-chain via `event_id → events.organisation_id`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `stage_days`
|
### `stage_days`
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
| ---------- | ------- | --------------------------------- |
|
| ----------- | ------- | ---------------------------------------------- |
|
||||||
| `id` | int AI | PK — integer for join performance |
|
| `id` | int AI | PK — integer for join performance |
|
||||||
| `stage_id` | ULID FK | → stages |
|
| `stage_id` | ULID FK | → stages |
|
||||||
| `day_date` | date | |
|
| `event_id` | ULID FK | → events (sub-event or flat event = "show host") |
|
||||||
|
|
||||||
**Unique constraint:** `UNIQUE(stage_id, day_date)`
|
**Unique:** `UNIQUE(stage_id, event_id)`
|
||||||
|
**Indexes:** `(event_id)`
|
||||||
|
**Soft delete:** no (pure pivot)
|
||||||
|
**Scope:** `OrganisationScope` (FK-chain via stage)
|
||||||
|
|
||||||
|
For a festival, each stage has multiple `stage_days` rows (one per
|
||||||
|
active sub-event). For a flat event, each stage has exactly one
|
||||||
|
row referencing the event itself.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `advance_sections`
|
### `artist_engagements` (per-event booking)
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
| ------------------- | ------------------ | -------------------------------------------------------------- |
|
| ---------------------------- | -------------------------- | ---------------------------------------------------- |
|
||||||
| `id` | ULID | PK |
|
| `id` | ULID | PK |
|
||||||
| `artist_id` | ULID FK | → artists |
|
| `organisation_id` | ULID FK | → organisations (denormalised — observer-maintained) |
|
||||||
| `name` | string | |
|
| `artist_id` | ULID FK | → artists (master) |
|
||||||
| `type` | enum | `guest_list\|contacts\|production\|custom` |
|
| `event_id` | ULID FK | → events (festival or flat event) |
|
||||||
| `is_open` | bool | |
|
| `booking_status` | string | RFC D9 — see `App\Enums\Artist\ArtistEngagementStatus` |
|
||||||
| `open_from` | datetime nullable | |
|
| `project_leader_id` | ULID FK nullable | → users |
|
||||||
| `open_to` | datetime nullable | |
|
| `fee_amount` | decimal(10,2) nullable | |
|
||||||
| `sort_order` | int | |
|
| `fee_currency` | string(3) default 'EUR' | |
|
||||||
| `submission_status` | enum | `open\|pending\|submitted\|approved\|declined` |
|
| `fee_type` | string nullable | `App\Enums\Artist\FeeType` |
|
||||||
| `last_submitted_at` | timestamp nullable | |
|
| `buma_applicable` | bool default true | |
|
||||||
| `last_submitted_by` | string nullable | |
|
| `buma_percentage` | decimal(5,2) default 7.00 | |
|
||||||
| `submission_diff` | JSON nullable | `{created, updated, untouched, deleted}` counts per submission |
|
| `buma_handled_by` | string default 'organisation' | `App\Enums\Artist\BumaHandledBy` (D26) |
|
||||||
|
| `vat_applicable` | bool default true | |
|
||||||
|
| `vat_percentage` | decimal(5,2) default 21.00 | |
|
||||||
|
| `deal_breakdown` | JSON nullable | opaque line-items |
|
||||||
|
| `deposit_percentage` | decimal(5,2) nullable | |
|
||||||
|
| `deposit_due_date` | date nullable | |
|
||||||
|
| `balance_due_date` | date nullable | |
|
||||||
|
| `payment_status` | string default 'none' | `App\Enums\Artist\PaymentStatus` |
|
||||||
|
| `crew_count` | int default 0 | |
|
||||||
|
| `guests_count` | int default 0 | |
|
||||||
|
| `requested_at` | datetime nullable | set when status → Requested |
|
||||||
|
| `option_expires_at` | datetime nullable | required when status=Option; demote-job uses this |
|
||||||
|
| `advance_open_from` | datetime nullable | |
|
||||||
|
| `advance_open_to` | datetime nullable | |
|
||||||
|
| `portal_token` | ULID unique nullable | tour-manager portal access |
|
||||||
|
| `advancing_completed_count` | int default 0 | observer-maintained (Session 3) |
|
||||||
|
| `advancing_total_count` | int default 0 | observer-maintained (Session 3) |
|
||||||
|
| `notes` | text nullable | |
|
||||||
|
| `created_at`, `updated_at`, `deleted_at` | | soft delete |
|
||||||
|
|
||||||
**Indexes:** `(artist_id, is_open)`, `(artist_id, submission_status)`
|
**Unique:** `UNIQUE(artist_id, event_id)`, `UNIQUE(portal_token)`
|
||||||
|
**Indexes:** `(organisation_id)`, `(event_id, booking_status)`, `(option_expires_at)`
|
||||||
|
**Soft delete:** yes (cascades to performances, advance_sections via `ArtistEngagementObserver`)
|
||||||
|
**Scope:** `OrganisationScope` (direct `organisation_id`)
|
||||||
|
**Observers:**
|
||||||
|
- `creating`: auto-fills `organisation_id` from `artist`, asserts `artist.organisation_id === event.organisation_id` (cross-tenant guard — `CrossTenantEngagementException`)
|
||||||
|
- `deleted`: cascade soft-deletes `performances`, hard-deletes `advance_sections`. `advance_submissions` rows are immutable audit records and remain attached.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `performances`
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| --------------- | -------------------------- | ---------------------------------------------------- |
|
||||||
|
| `id` | ULID | PK |
|
||||||
|
| `engagement_id` | ULID FK | → artist_engagements |
|
||||||
|
| `event_id` | ULID FK | → events (sub-event or flat event = "show host") |
|
||||||
|
| `stage_id` | ULID FK nullable | → stages; NULL = parked / wachtrij (D13) |
|
||||||
|
| `lane` | unsigned tinyint default 0 | (D13) |
|
||||||
|
| `start_at` | datetime | ⩾ event.start, < event.end |
|
||||||
|
| `end_at` | datetime | > start_at, ⩽ event.end |
|
||||||
|
| `version` | int default 0 | optimistic-lock counter — observer-maintained (D14) |
|
||||||
|
| `notes` | text nullable | |
|
||||||
|
| `created_at`, `updated_at`, `deleted_at` | | soft delete |
|
||||||
|
|
||||||
|
**Indexes:** `(event_id, stage_id, start_at, end_at)`, `(engagement_id)`, `(stage_id, start_at)` (lane resolver)
|
||||||
|
**Soft delete:** yes (cascade with engagement via observer)
|
||||||
|
**Scope:** `OrganisationScope` (FK-chain via `engagement_id → artist_engagements.organisation_id`)
|
||||||
|
**Observer (`PerformanceObserver`):** increments `version` by 1 on every UPDATE (D14 optimistic lock — `MoveTimetablePerformanceRequest` in Session 2 compares against client-supplied version).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `advance_sections` (engagement-scoped)
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
| ------------------- | ------------------- | -------------------------------------------------------------- |
|
||||||
|
| `id` | ULID | PK |
|
||||||
|
| `engagement_id` | ULID FK | → artist_engagements (was `artist_id` in the pre-v0.2 plan) |
|
||||||
|
| `name` | string(80) | |
|
||||||
|
| `type` | string | `App\Enums\Artist\AdvanceSectionType` |
|
||||||
|
| `is_open` | bool default false | |
|
||||||
|
| `open_from` | datetime nullable | |
|
||||||
|
| `open_to` | datetime nullable | |
|
||||||
|
| `sort_order` | int default 0 | |
|
||||||
|
| `submission_status` | string default 'open' | `App\Enums\Artist\AdvanceSectionSubmissionStatus` |
|
||||||
|
| `last_submitted_at` | timestamp nullable | |
|
||||||
|
| `last_submitted_by` | string nullable | tour-manager name from form |
|
||||||
|
| `submission_diff` | JSON nullable | `{created, updated, untouched, deleted}` counts per submission |
|
||||||
|
| `created_at`, `updated_at` | timestamps | |
|
||||||
|
|
||||||
|
**Indexes:** `(engagement_id, is_open)`, `(engagement_id, submission_status)`
|
||||||
|
**Soft delete:** no — hard-deleted with the parent engagement (RFC §5.4)
|
||||||
|
**Scope:** `OrganisationScope` (FK-chain via engagement)
|
||||||
|
|
||||||
|
The shift from `artist_id` (master) to `engagement_id` (per-event) is
|
||||||
|
the schema correction that allows multi-event artists to advance
|
||||||
|
each engagement independently. The `form_submission` subject remains
|
||||||
|
the master `Artist` (per `PurposeRegistry` §17.3) — see
|
||||||
|
`ARCH-FORM-BUILDER.md §3.2.5` for the resolver wiring.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `advance_submissions`
|
### `advance_submissions`
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
| -------------------- | ------------------ | ------------------------------ |
|
| -------------------- | --------------------- | ------------------------------ |
|
||||||
| `id` | ULID | PK |
|
| `id` | ULID | PK |
|
||||||
| `advance_section_id` | ULID FK | → advance_sections |
|
| `advance_section_id` | ULID FK | → advance_sections |
|
||||||
| `submitted_by_name` | string | |
|
| `submitted_by_name` | string | |
|
||||||
| `submitted_by_email` | string | |
|
| `submitted_by_email` | string | |
|
||||||
| `submitted_at` | timestamp | |
|
| `submitted_at` | timestamp | |
|
||||||
| `status` | enum | `pending\|accepted\|declined` |
|
| `status` | string default 'pending' | `App\Enums\Artist\AdvanceSubmissionStatus` |
|
||||||
| `reviewed_by` | ULID FK nullable | → users |
|
| `reviewed_by` | ULID FK nullable | → users |
|
||||||
| `reviewed_at` | timestamp nullable | |
|
| `reviewed_at` | timestamp nullable | |
|
||||||
| `data` | JSON | Free form data — not queryable |
|
| `data` | JSON | free-form payload — not queryable |
|
||||||
|
| `created_at`, `updated_at` | timestamps | |
|
||||||
|
|
||||||
**Indexes:** `(advance_section_id, status)`
|
**Indexes:** `(advance_section_id, status)`
|
||||||
|
**Soft delete:** no — audit-immutable (RFC §5.4). Survive engagement deletion via FK; no application code mutates these rows after creation.
|
||||||
|
**Scope:** `OrganisationScope` (FK-chain via section → engagement)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `artist_contacts`
|
### Enums (under `App\Enums\Artist\`)
|
||||||
|
|
||||||
| Column | Type | Notes |
|
| Enum | Values | Notes |
|
||||||
| -------------------- | --------------- | -------------------------------- |
|
| --------------------------------- | ----------------------------------------------------------------------------------------- | ----- |
|
||||||
| `id` | ULID | PK |
|
| `ArtistEngagementStatus` | `draft`, `requested`, `option`, `offered`, `confirmed`, `contracted`, `cancelled`, `rejected`, `declined` | RFC D9 — Dutch labels via `label()` |
|
||||||
| `artist_id` | ULID FK | → artists |
|
| `BumaHandledBy` | `organisation`, `booking_agency`, `not_applicable` | RFC D26 |
|
||||||
| `name` | string | |
|
| `FeeType` | `flat`, `door_split`, `guarantee_plus_split` | |
|
||||||
| `email` | string nullable | |
|
| `PaymentStatus` | `none`, `deposit_paid`, `paid_in_full` | |
|
||||||
| `phone` | string nullable | |
|
| `AdvanceSectionType` | `guest_list`, `contacts`, `production`, `custom` | |
|
||||||
| `role` | string | e.g. tour manager, agent, booker |
|
| `AdvanceSectionSubmissionStatus` | `open`, `pending`, `submitted`, `approved`, `declined` | |
|
||||||
| `receives_briefing` | bool | |
|
| `AdvanceSubmissionStatus` | `pending`, `accepted`, `declined` | |
|
||||||
| `receives_infosheet` | bool | |
|
|
||||||
| `is_travel_party` | bool | |
|
|
||||||
|
|
||||||
**Indexes:** `(artist_id)`
|
> **Riders + itineraries:** the previous §3.5.7 plan included
|
||||||
|
> `artist_riders` and `itinerary_items` tables. RFC v0.2 §5.3 does
|
||||||
---
|
> NOT create them; rider data lives in advance-section submissions
|
||||||
|
> (free-form JSON), and itineraries are deferred to a future RFC.
|
||||||
### `artist_riders`
|
|
||||||
|
|
||||||
| Column | Type | Notes |
|
|
||||||
| ----------- | ------- | ------------------------ |
|
|
||||||
| `id` | ULID | PK |
|
|
||||||
| `artist_id` | ULID FK | → artists |
|
|
||||||
| `category` | enum | `technical\|hospitality` |
|
|
||||||
| `items` | JSON | Unstructured rider data |
|
|
||||||
|
|
||||||
**Indexes:** `(artist_id, category)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `itinerary_items`
|
|
||||||
|
|
||||||
| Column | Type | Notes |
|
|
||||||
| --------------- | --------------- | -------------------------------------------------- |
|
|
||||||
| `id` | ULID | PK |
|
|
||||||
| `artist_id` | ULID FK | → artists |
|
|
||||||
| `type` | enum | `transfer\|pickup\|delivery\|checkin\|performance` |
|
|
||||||
| `datetime` | datetime | |
|
|
||||||
| `from_location` | string nullable | |
|
|
||||||
| `to_location` | string nullable | |
|
|
||||||
| `notes` | text nullable | |
|
|
||||||
|
|
||||||
**Indexes:** `(artist_id, datetime)`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user