From c31f2ba7840f298fb8ac4b86887f618f4f19514a Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 17:51:11 +0200 Subject: [PATCH 01/14] chore(timetable): remove pre-RFC-v0.2 artist/advance_sections migration stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...2026_04_08_170000_create_artists_table.php | 43 ------------------- ...8_180000_create_advance_sections_table.php | 37 ---------------- 2 files changed, 80 deletions(-) delete mode 100644 api/database/migrations/2026_04_08_170000_create_artists_table.php delete mode 100644 api/database/migrations/2026_04_08_180000_create_advance_sections_table.php diff --git a/api/database/migrations/2026_04_08_170000_create_artists_table.php b/api/database/migrations/2026_04_08_170000_create_artists_table.php deleted file mode 100644 index 47268bbc..00000000 --- a/api/database/migrations/2026_04_08_170000_create_artists_table.php +++ /dev/null @@ -1,43 +0,0 @@ -ulid('id')->primary(); - $table->foreignUlid('event_id')->constrained()->cascadeOnDelete(); - $table->string('name'); - $table->enum('booking_status', ['concept', 'requested', 'option', 'confirmed', 'contracted', 'cancelled'])->default('concept'); - $table->tinyInteger('star_rating')->default(1); - $table->foreignUlid('project_leader_id')->nullable()->constrained('users')->nullOnDelete(); - $table->boolean('milestone_offer_in')->default(false); - $table->boolean('milestone_offer_agreed')->default(false); - $table->boolean('milestone_confirmed')->default(false); - $table->boolean('milestone_announced')->default(false); - $table->boolean('milestone_schedule_confirmed')->default(false); - $table->boolean('milestone_itinerary_sent')->default(false); - $table->boolean('milestone_advance_sent')->default(false); - $table->boolean('milestone_advance_received')->default(false); - $table->datetime('advance_open_from')->nullable(); - $table->datetime('advance_open_to')->nullable(); - $table->boolean('show_advance_share_page')->default(true); - $table->char('portal_token', 26)->unique(); - $table->timestamps(); - $table->softDeletes(); - - $table->index('event_id'); - }); - } - - public function down(): void - { - Schema::dropIfExists('artists'); - } -}; diff --git a/api/database/migrations/2026_04_08_180000_create_advance_sections_table.php b/api/database/migrations/2026_04_08_180000_create_advance_sections_table.php deleted file mode 100644 index 8e4ba041..00000000 --- a/api/database/migrations/2026_04_08_180000_create_advance_sections_table.php +++ /dev/null @@ -1,37 +0,0 @@ -ulid('id')->primary(); - $table->foreignUlid('artist_id')->constrained()->cascadeOnDelete(); - $table->string('name'); - $table->enum('type', ['guest_list', 'contacts', 'production', 'custom']); - $table->boolean('is_open')->default(false); - $table->datetime('open_from')->nullable(); - $table->datetime('open_to')->nullable(); - $table->unsignedInteger('sort_order')->default(0); - $table->enum('submission_status', ['open', 'pending', 'submitted', 'approved', 'declined'])->default('open'); - $table->timestamp('last_submitted_at')->nullable(); - $table->string('last_submitted_by')->nullable(); - $table->json('submission_diff')->nullable(); - $table->timestamps(); - - $table->index(['artist_id', 'is_open']); - $table->index(['artist_id', 'submission_status']); - }); - } - - public function down(): void - { - Schema::dropIfExists('advance_sections'); - } -}; -- 2.39.5 From 0c03c449c3e959a36da1a5c2f3493fd51fb52019 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 17:55:34 +0200 Subject: [PATCH 02/14] =?UTF-8?q?feat(timetable):=20RFC=20v0.2=20=C2=A75.3?= =?UTF-8?q?=20migrations=20=E2=80=94=20artists,=20engagements,=20stages,?= =?UTF-8?q?=20performances,=20advancing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../2026_05_08_100000_create_genres_table.php | 30 + ...2026_05_08_100001_create_artists_table.php | 38 + ...02_add_handles_buma_to_companies_table.php | 24 + ...08_100003_create_artist_contacts_table.php | 33 + .../2026_05_08_100004_create_stages_table.php | 31 + ...6_05_08_100005_create_stage_days_table.php | 27 + ...100006_create_artist_engagements_table.php | 69 + ...05_08_100007_create_performances_table.php | 36 + ...8_100008_create_advance_sections_table.php | 37 + ...00009_create_advance_submissions_table.php | 33 + api/database/schema/mysql-schema.sql | 1334 ++++++++++------- 11 files changed, 1109 insertions(+), 583 deletions(-) create mode 100644 api/database/migrations/2026_05_08_100000_create_genres_table.php create mode 100644 api/database/migrations/2026_05_08_100001_create_artists_table.php create mode 100644 api/database/migrations/2026_05_08_100002_add_handles_buma_to_companies_table.php create mode 100644 api/database/migrations/2026_05_08_100003_create_artist_contacts_table.php create mode 100644 api/database/migrations/2026_05_08_100004_create_stages_table.php create mode 100644 api/database/migrations/2026_05_08_100005_create_stage_days_table.php create mode 100644 api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php create mode 100644 api/database/migrations/2026_05_08_100007_create_performances_table.php create mode 100644 api/database/migrations/2026_05_08_100008_create_advance_sections_table.php create mode 100644 api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php diff --git a/api/database/migrations/2026_05_08_100000_create_genres_table.php b/api/database/migrations/2026_05_08_100000_create_genres_table.php new file mode 100644 index 00000000..a43fd86b --- /dev/null +++ b/api/database/migrations/2026_05_08_100000_create_genres_table.php @@ -0,0 +1,30 @@ +ulid('id')->primary(); + $table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete(); + $table->string('name', 40); + $table->string('color', 7)->nullable(); + $table->integer('sort_order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->unique(['organisation_id', 'name']); + }); + } + + public function down(): void + { + Schema::dropIfExists('genres'); + } +}; diff --git a/api/database/migrations/2026_05_08_100001_create_artists_table.php b/api/database/migrations/2026_05_08_100001_create_artists_table.php new file mode 100644 index 00000000..b63c00fe --- /dev/null +++ b/api/database/migrations/2026_05_08_100001_create_artists_table.php @@ -0,0 +1,38 @@ +ulid('id')->primary(); + $table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete(); + $table->string('name', 120); + $table->string('slug', 120); + $table->foreignUlid('default_genre_id')->nullable()->constrained('genres')->nullOnDelete(); + $table->integer('default_draw')->nullable(); + $table->tinyInteger('star_rating')->nullable(); + $table->string('home_base_country', 2)->nullable(); + $table->foreignUlid('agent_company_id')->nullable()->constrained('companies')->nullOnDelete(); + $table->text('notes')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['organisation_id', 'slug']); + $table->index(['organisation_id', 'name']); + $table->index('default_genre_id'); + $table->index('agent_company_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('artists'); + } +}; diff --git a/api/database/migrations/2026_05_08_100002_add_handles_buma_to_companies_table.php b/api/database/migrations/2026_05_08_100002_add_handles_buma_to_companies_table.php new file mode 100644 index 00000000..6343e035 --- /dev/null +++ b/api/database/migrations/2026_05_08_100002_add_handles_buma_to_companies_table.php @@ -0,0 +1,24 @@ +boolean('handles_buma')->default(false)->after('type'); + }); + } + + public function down(): void + { + Schema::table('companies', function (Blueprint $table) { + $table->dropColumn('handles_buma'); + }); + } +}; diff --git a/api/database/migrations/2026_05_08_100003_create_artist_contacts_table.php b/api/database/migrations/2026_05_08_100003_create_artist_contacts_table.php new file mode 100644 index 00000000..7d4187b0 --- /dev/null +++ b/api/database/migrations/2026_05_08_100003_create_artist_contacts_table.php @@ -0,0 +1,33 @@ +ulid('id')->primary(); + $table->foreignUlid('artist_id')->constrained()->cascadeOnDelete(); + $table->string('name', 120); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->string('role', 60); + $table->boolean('is_primary')->default(false); + $table->boolean('receives_briefing')->default(false); + $table->boolean('receives_infosheet')->default(false); + $table->timestamps(); + + $table->index(['artist_id', 'role']); + }); + } + + public function down(): void + { + Schema::dropIfExists('artist_contacts'); + } +}; diff --git a/api/database/migrations/2026_05_08_100004_create_stages_table.php b/api/database/migrations/2026_05_08_100004_create_stages_table.php new file mode 100644 index 00000000..57b2e9a1 --- /dev/null +++ b/api/database/migrations/2026_05_08_100004_create_stages_table.php @@ -0,0 +1,31 @@ +ulid('id')->primary(); + $table->foreignUlid('event_id')->constrained()->cascadeOnDelete(); + $table->string('name', 120); + $table->string('color', 7); + $table->integer('capacity')->nullable(); + $table->integer('sort_order')->default(0); + $table->timestamps(); + + $table->unique(['event_id', 'name']); + $table->index(['event_id', 'sort_order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('stages'); + } +}; diff --git a/api/database/migrations/2026_05_08_100005_create_stage_days_table.php b/api/database/migrations/2026_05_08_100005_create_stage_days_table.php new file mode 100644 index 00000000..a9e220e8 --- /dev/null +++ b/api/database/migrations/2026_05_08_100005_create_stage_days_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignUlid('stage_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('event_id')->constrained()->cascadeOnDelete(); + + $table->unique(['stage_id', 'event_id']); + $table->index('event_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('stage_days'); + } +}; diff --git a/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php b/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php new file mode 100644 index 00000000..7d8c9d58 --- /dev/null +++ b/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php @@ -0,0 +1,69 @@ +ulid('id')->primary(); + $table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('artist_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('event_id')->constrained()->cascadeOnDelete(); + + $table->string('booking_status')->default('draft'); + $table->foreignUlid('project_leader_id')->nullable()->constrained('users')->nullOnDelete(); + + // Deal info + $table->decimal('fee_amount', 10, 2)->nullable(); + $table->string('fee_currency', 3)->default('EUR'); + $table->string('fee_type')->nullable(); + $table->boolean('buma_applicable')->default(true); + $table->decimal('buma_percentage', 5, 2)->default(7.00); + $table->string('buma_handled_by')->default('organisation'); + $table->boolean('vat_applicable')->default(true); + $table->decimal('vat_percentage', 5, 2)->default(21.00); + $table->json('deal_breakdown')->nullable(); + $table->decimal('deposit_percentage', 5, 2)->nullable(); + $table->date('deposit_due_date')->nullable(); + $table->date('balance_due_date')->nullable(); + $table->string('payment_status')->default('none'); + + // Crew + guests + $table->integer('crew_count')->default(0); + $table->integer('guests_count')->default(0); + + // Milestone datetimes (per RFC v0.2 §5.3) + $table->datetime('requested_at')->nullable(); + $table->datetime('option_expires_at')->nullable(); + $table->datetime('advance_open_from')->nullable(); + $table->datetime('advance_open_to')->nullable(); + + // Portal access + $table->ulid('portal_token')->nullable()->unique(); + + // Advancing aggregates (recomputed in Session 3) + $table->integer('advancing_completed_count')->default(0); + $table->integer('advancing_total_count')->default(0); + + $table->text('notes')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['artist_id', 'event_id']); + $table->index('organisation_id'); + $table->index(['event_id', 'booking_status']); + $table->index('option_expires_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('artist_engagements'); + } +}; diff --git a/api/database/migrations/2026_05_08_100007_create_performances_table.php b/api/database/migrations/2026_05_08_100007_create_performances_table.php new file mode 100644 index 00000000..b96aa2c6 --- /dev/null +++ b/api/database/migrations/2026_05_08_100007_create_performances_table.php @@ -0,0 +1,36 @@ +ulid('id')->primary(); + $table->foreignUlid('engagement_id')->constrained('artist_engagements')->cascadeOnDelete(); + $table->foreignUlid('event_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('stage_id')->nullable()->constrained()->nullOnDelete(); + $table->unsignedTinyInteger('lane')->default(0); + $table->datetime('start_at'); + $table->datetime('end_at'); + $table->integer('version')->default(0); + $table->text('notes')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['event_id', 'stage_id', 'start_at', 'end_at']); + $table->index('engagement_id'); + $table->index(['stage_id', 'start_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('performances'); + } +}; diff --git a/api/database/migrations/2026_05_08_100008_create_advance_sections_table.php b/api/database/migrations/2026_05_08_100008_create_advance_sections_table.php new file mode 100644 index 00000000..ae955c75 --- /dev/null +++ b/api/database/migrations/2026_05_08_100008_create_advance_sections_table.php @@ -0,0 +1,37 @@ +ulid('id')->primary(); + $table->foreignUlid('engagement_id')->constrained('artist_engagements')->cascadeOnDelete(); + $table->string('name', 80); + $table->string('type'); + $table->boolean('is_open')->default(false); + $table->datetime('open_from')->nullable(); + $table->datetime('open_to')->nullable(); + $table->integer('sort_order')->default(0); + $table->string('submission_status')->default('open'); + $table->timestamp('last_submitted_at')->nullable(); + $table->string('last_submitted_by')->nullable(); + $table->json('submission_diff')->nullable(); + $table->timestamps(); + + $table->index(['engagement_id', 'is_open']); + $table->index(['engagement_id', 'submission_status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('advance_sections'); + } +}; diff --git a/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php b/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php new file mode 100644 index 00000000..1bf9f4a9 --- /dev/null +++ b/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php @@ -0,0 +1,33 @@ +ulid('id')->primary(); + $table->foreignUlid('advance_section_id')->constrained()->cascadeOnDelete(); + $table->string('submitted_by_name'); + $table->string('submitted_by_email'); + $table->timestamp('submitted_at'); + $table->string('status')->default('pending'); + $table->foreignUlid('reviewed_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('reviewed_at')->nullable(); + $table->json('data'); + $table->timestamps(); + + $table->index(['advance_section_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('advance_submissions'); + } +}; diff --git a/api/database/schema/mysql-schema.sql b/api/database/schema/mysql-schema.sql index 8daf9e65..41f58fae 100644 --- a/api/database/schema/mysql-schema.sql +++ b/api/database/schema/mysql-schema.sql @@ -9,13 +9,13 @@ DROP TABLE IF EXISTS `activity_log`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `activity_log` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `log_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `subject_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `subject_id` varchar(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `event` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `causer_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `causer_id` varchar(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `log_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `subject_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subject_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `event` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `causer_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `causer_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `attribute_changes` json DEFAULT NULL, `properties` json DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -30,65 +30,150 @@ DROP TABLE IF EXISTS `advance_sections`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `advance_sections` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `artist_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `type` enum('guest_list','contacts','production','custom') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `engagement_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(80) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `is_open` tinyint(1) NOT NULL DEFAULT '0', `open_from` datetime DEFAULT NULL, `open_to` datetime DEFAULT NULL, - `sort_order` int unsigned NOT NULL DEFAULT '0', - `submission_status` enum('open','pending','submitted','approved','declined') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open', + `sort_order` int NOT NULL DEFAULT '0', + `submission_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open', `last_submitted_at` timestamp NULL DEFAULT NULL, - `last_submitted_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `last_submitted_by` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `submission_diff` json DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), - KEY `advance_sections_artist_id_is_open_index` (`artist_id`,`is_open`), - KEY `advance_sections_artist_id_submission_status_index` (`artist_id`,`submission_status`), - CONSTRAINT `advance_sections_artist_id_foreign` FOREIGN KEY (`artist_id`) REFERENCES `artists` (`id`) ON DELETE CASCADE + KEY `advance_sections_engagement_id_is_open_index` (`engagement_id`,`is_open`), + KEY `advance_sections_engagement_id_submission_status_index` (`engagement_id`,`submission_status`), + CONSTRAINT `advance_sections_engagement_id_foreign` FOREIGN KEY (`engagement_id`) REFERENCES `artist_engagements` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `advance_submissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `advance_submissions` ( + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `advance_section_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `submitted_by_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `submitted_by_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `submitted_at` timestamp NOT NULL, + `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', + `reviewed_by` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reviewed_at` timestamp NULL DEFAULT NULL, + `data` json NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `advance_submissions_reviewed_by_foreign` (`reviewed_by`), + KEY `advance_submissions_advance_section_id_status_index` (`advance_section_id`,`status`), + CONSTRAINT `advance_submissions_advance_section_id_foreign` FOREIGN KEY (`advance_section_id`) REFERENCES `advance_sections` (`id`) ON DELETE CASCADE, + CONSTRAINT `advance_submissions_reviewed_by_foreign` FOREIGN KEY (`reviewed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `artist_contacts`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `artist_contacts` ( + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `artist_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `phone` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `role` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL, + `is_primary` tinyint(1) NOT NULL DEFAULT '0', + `receives_briefing` tinyint(1) NOT NULL DEFAULT '0', + `receives_infosheet` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `artist_contacts_artist_id_role_index` (`artist_id`,`role`), + CONSTRAINT `artist_contacts_artist_id_foreign` FOREIGN KEY (`artist_id`) REFERENCES `artists` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `artist_engagements`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `artist_engagements` ( + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `artist_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `booking_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'draft', + `project_leader_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `fee_amount` decimal(10,2) DEFAULT NULL, + `fee_currency` varchar(3) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'EUR', + `fee_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `buma_applicable` tinyint(1) NOT NULL DEFAULT '1', + `buma_percentage` decimal(5,2) NOT NULL DEFAULT '7.00', + `buma_handled_by` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'organisation', + `vat_applicable` tinyint(1) NOT NULL DEFAULT '1', + `vat_percentage` decimal(5,2) NOT NULL DEFAULT '21.00', + `deal_breakdown` json DEFAULT NULL, + `deposit_percentage` decimal(5,2) DEFAULT NULL, + `deposit_due_date` date DEFAULT NULL, + `balance_due_date` date DEFAULT NULL, + `payment_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'none', + `crew_count` int NOT NULL DEFAULT '0', + `guests_count` int NOT NULL DEFAULT '0', + `requested_at` datetime DEFAULT NULL, + `option_expires_at` datetime DEFAULT NULL, + `advance_open_from` datetime DEFAULT NULL, + `advance_open_to` datetime DEFAULT NULL, + `portal_token` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `advancing_completed_count` int NOT NULL DEFAULT '0', + `advancing_total_count` int NOT NULL DEFAULT '0', + `notes` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `artist_engagements_artist_id_event_id_unique` (`artist_id`,`event_id`), + UNIQUE KEY `artist_engagements_portal_token_unique` (`portal_token`), + KEY `artist_engagements_project_leader_id_foreign` (`project_leader_id`), + KEY `artist_engagements_organisation_id_index` (`organisation_id`), + KEY `artist_engagements_event_id_booking_status_index` (`event_id`,`booking_status`), + KEY `artist_engagements_option_expires_at_index` (`option_expires_at`), + CONSTRAINT `artist_engagements_artist_id_foreign` FOREIGN KEY (`artist_id`) REFERENCES `artists` (`id`) ON DELETE CASCADE, + CONSTRAINT `artist_engagements_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE, + CONSTRAINT `artist_engagements_organisation_id_foreign` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`) ON DELETE CASCADE, + CONSTRAINT `artist_engagements_project_leader_id_foreign` FOREIGN KEY (`project_leader_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `artists`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `artists` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `booking_status` enum('concept','requested','option','confirmed','contracted','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'concept', - `star_rating` tinyint NOT NULL DEFAULT '1', - `project_leader_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `milestone_offer_in` tinyint(1) NOT NULL DEFAULT '0', - `milestone_offer_agreed` tinyint(1) NOT NULL DEFAULT '0', - `milestone_confirmed` tinyint(1) NOT NULL DEFAULT '0', - `milestone_announced` tinyint(1) NOT NULL DEFAULT '0', - `milestone_schedule_confirmed` tinyint(1) NOT NULL DEFAULT '0', - `milestone_itinerary_sent` tinyint(1) NOT NULL DEFAULT '0', - `milestone_advance_sent` tinyint(1) NOT NULL DEFAULT '0', - `milestone_advance_received` tinyint(1) NOT NULL DEFAULT '0', - `advance_open_from` datetime DEFAULT NULL, - `advance_open_to` datetime DEFAULT NULL, - `show_advance_share_page` tinyint(1) NOT NULL DEFAULT '1', - `portal_token` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `default_genre_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `default_draw` int DEFAULT NULL, + `star_rating` tinyint DEFAULT NULL, + `home_base_country` varchar(2) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `agent_company_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `notes` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `artists_portal_token_unique` (`portal_token`), - KEY `artists_project_leader_id_foreign` (`project_leader_id`), - KEY `artists_event_id_index` (`event_id`), - CONSTRAINT `artists_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE, - CONSTRAINT `artists_project_leader_id_foreign` FOREIGN KEY (`project_leader_id`) REFERENCES `users` (`id`) ON DELETE SET NULL + UNIQUE KEY `artists_organisation_id_slug_unique` (`organisation_id`,`slug`), + KEY `artists_organisation_id_name_index` (`organisation_id`,`name`), + KEY `artists_default_genre_id_index` (`default_genre_id`), + KEY `artists_agent_company_id_index` (`agent_company_id`), + CONSTRAINT `artists_agent_company_id_foreign` FOREIGN KEY (`agent_company_id`) REFERENCES `companies` (`id`) ON DELETE SET NULL, + CONSTRAINT `artists_default_genre_id_foreign` FOREIGN KEY (`default_genre_id`) REFERENCES `genres` (`id`) ON DELETE SET NULL, + CONSTRAINT `artists_organisation_id_foreign` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `cache`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `cache` ( - `key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `value` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, `expiration` int NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -97,8 +182,8 @@ DROP TABLE IF EXISTS `cache_locks`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `cache_locks` ( - `key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `expiration` int NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -107,15 +192,16 @@ DROP TABLE IF EXISTS `companies`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `companies` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `type` enum('supplier','partner','agency','venue','other') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `kvk_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `contact_first_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `contact_last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `contact_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `contact_phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` enum('supplier','partner','agency','venue','other') COLLATE utf8mb4_unicode_ci NOT NULL, + `handles_buma` tinyint(1) NOT NULL DEFAULT '0', + `kvk_number` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `contact_first_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `contact_last_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `contact_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `contact_phone` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, @@ -129,11 +215,11 @@ DROP TABLE IF EXISTS `crowd_list_persons`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `crowd_list_persons` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `crowd_list_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `crowd_list_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `added_at` timestamp NOT NULL, - `added_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `added_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `crowd_list_persons_crowd_list_id_person_id_unique` (`crowd_list_id`,`person_id`), KEY `crowd_list_persons_added_by_user_id_foreign` (`added_by_user_id`), @@ -147,12 +233,12 @@ DROP TABLE IF EXISTS `crowd_lists`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `crowd_lists` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `crowd_type_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `type` enum('internal','external') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `recipient_company_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `crowd_type_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` enum('internal','external') COLLATE utf8mb4_unicode_ci NOT NULL, + `recipient_company_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `auto_approve` tinyint(1) NOT NULL DEFAULT '0', `max_persons` int unsigned DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -170,12 +256,12 @@ DROP TABLE IF EXISTS `crowd_types`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `crowd_types` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `system_type` enum('CREW','GUEST','ARTIST','VOLUNTEER','PRESS','PARTNER','SUPPLIER') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `color` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#6366f1', - `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `system_type` enum('CREW','GUEST','ARTIST','VOLUNTEER','PRESS','PARTNER','SUPPLIER') COLLATE utf8mb4_unicode_ci NOT NULL, + `color` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#6366f1', + `icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_active` tinyint(1) NOT NULL DEFAULT '1', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -188,13 +274,13 @@ DROP TABLE IF EXISTS `email_change_requests`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `email_change_requests` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `current_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `new_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `requested_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `current_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `new_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `requested_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', `expires_at` timestamp NOT NULL, `verified_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -211,22 +297,22 @@ DROP TABLE IF EXISTS `email_logs`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `email_logs` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `recipient_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `recipient_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `mailable_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `template_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'queued', - `error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `recipient_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `recipient_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mailable_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `template_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'queued', + `error_message` text COLLATE utf8mb4_unicode_ci, `queued_at` timestamp NOT NULL, `sent_at` timestamp NULL DEFAULT NULL, `failed_at` timestamp NULL DEFAULT NULL, - `triggered_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `triggered_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -243,9 +329,9 @@ DROP TABLE IF EXISTS `event_person_activations`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `event_person_activations` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `event_person_activations_event_id_person_id_unique` (`event_id`,`person_id`), KEY `event_person_activations_person_id_index` (`person_id`), @@ -258,10 +344,10 @@ DROP TABLE IF EXISTS `event_user_roles`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `event_user_roles` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `role` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -275,24 +361,24 @@ DROP TABLE IF EXISTS `events`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `events` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `parent_event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `parent_event_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `start_date` date NOT NULL, `end_date` date NOT NULL, - `timezone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Europe/Amsterdam', - `status` enum('draft','published','registration_open','buildup','showday','teardown','closed') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'draft', - `event_type` enum('event','festival','series') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'event', - `event_type_label` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `sub_event_label` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `timezone` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Europe/Amsterdam', + `status` enum('draft','published','registration_open','buildup','showday','teardown','closed') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'draft', + `event_type` enum('event','festival','series') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'event', + `event_type_label` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `sub_event_label` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_recurring` tinyint(1) NOT NULL DEFAULT '0', - `recurrence_rule` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `recurrence_rule` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `recurrence_exceptions` json DEFAULT NULL, - `registration_banner_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `registration_welcome_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `registration_logo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `registration_banner_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `registration_welcome_text` text COLLATE utf8mb4_unicode_ci, + `registration_logo_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `registration_show_section_preferences` tinyint(1) NOT NULL DEFAULT '1', `registration_show_availability` tinyint(1) NOT NULL DEFAULT '1', `created_at` timestamp NULL DEFAULT NULL, @@ -311,11 +397,11 @@ DROP TABLE IF EXISTS `failed_jobs`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `failed_jobs` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `uuid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `connection` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `queue` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `exception` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`) @@ -325,23 +411,23 @@ DROP TABLE IF EXISTS `festival_sections`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `festival_sections` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `type` enum('standard','cross_event') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'standard', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `category` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `type` enum('standard','cross_event') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'standard', `crew_need` int unsigned DEFAULT NULL, `sort_order` int unsigned NOT NULL DEFAULT '0', `crew_auto_accepts` tinyint(1) NOT NULL DEFAULT '0', `crew_invited_to_events` tinyint(1) NOT NULL DEFAULT '0', `added_to_timeline` tinyint(1) NOT NULL DEFAULT '0', `responder_self_checkin` tinyint(1) NOT NULL DEFAULT '1', - `crew_accreditation_level` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `public_form_accreditation_level` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `crew_accreditation_level` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `public_form_accreditation_level` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `timed_accreditations` tinyint(1) NOT NULL DEFAULT '0', `show_in_registration` tinyint(1) NOT NULL DEFAULT '0', - `registration_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `registration_description` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, @@ -355,14 +441,14 @@ DROP TABLE IF EXISTS `form_field_bindings`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_bindings` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `target_entity` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `target_attribute` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `sync_direction` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `merge_strategy` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'overwrite', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `target_entity` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `target_attribute` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `mode` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `sync_direction` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `merge_strategy` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'overwrite', `trust_level` tinyint unsigned NOT NULL DEFAULT '50', `is_identity_key` tinyint(1) NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT NULL, @@ -377,10 +463,10 @@ DROP TABLE IF EXISTS `form_field_conditional_logic_conditions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_conditional_logic_conditions` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `group_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `field_slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `comparison_operator` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `group_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `field_slug` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `comparison_operator` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, `value` json DEFAULT NULL, `sort_order` int unsigned NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT NULL, @@ -395,10 +481,10 @@ DROP TABLE IF EXISTS `form_field_conditional_logic_groups`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_conditional_logic_groups` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_field_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `parent_group_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `operator` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_field_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `parent_group_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `operator` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL, `sort_order` int unsigned NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -413,10 +499,10 @@ DROP TABLE IF EXISTS `form_field_configs`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_configs` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `config_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `config_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, `parameters` json NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -430,17 +516,17 @@ DROP TABLE IF EXISTS `form_field_library`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_library` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `field_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `label` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `help_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `field_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `label` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `help_text` text COLLATE utf8mb4_unicode_ci, `default_is_required` tinyint(1) NOT NULL DEFAULT '0', `default_is_filterable` tinyint(1) NOT NULL DEFAULT '0', `translations` json DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `description` text COLLATE utf8mb4_unicode_ci, `usage_count` int unsigned NOT NULL DEFAULT '0', `is_system` tinyint(1) NOT NULL DEFAULT '0', `is_active` tinyint(1) NOT NULL DEFAULT '1', @@ -457,11 +543,11 @@ DROP TABLE IF EXISTS `form_field_options`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_options` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `label` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `value` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `label` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `sort_order` int unsigned NOT NULL DEFAULT '0', `translations` json DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -475,12 +561,12 @@ DROP TABLE IF EXISTS `form_field_validation_rules`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_field_validation_rules` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `rule_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `rule_type` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, `parameters` json NOT NULL, - `error_message_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `error_message_key` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -493,25 +579,25 @@ DROP TABLE IF EXISTS `form_fields`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_fields` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_section_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `library_field_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `field_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `label` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `help_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `section` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_section_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `library_field_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `field_type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `label` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `help_text` text COLLATE utf8mb4_unicode_ci, + `section` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_required` tinyint(1) NOT NULL DEFAULT '0', `is_filterable` tinyint(1) NOT NULL DEFAULT '0', `is_portal_visible` tinyint(1) NOT NULL DEFAULT '1', `is_admin_only` tinyint(1) NOT NULL DEFAULT '0', `is_unique` tinyint(1) NOT NULL DEFAULT '0', `is_pii` tinyint(1) NOT NULL DEFAULT '0', - `display_width` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'full', + `display_width` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'full', `role_restrictions` json DEFAULT NULL, `translations` json DEFAULT NULL, - `value_storage_hint` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'json', + `value_storage_hint` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'json', `review_required` tinyint(1) NOT NULL DEFAULT '0', `sort_order` int unsigned NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT NULL, @@ -532,14 +618,14 @@ DROP TABLE IF EXISTS `form_schema_sections`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_schema_sections` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci, `sort_order` int unsigned NOT NULL DEFAULT '0', `submit_independent` tinyint(1) NOT NULL DEFAULT '1', - `depends_on_section_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `depends_on_section_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `required_for_schema_submit` tinyint(1) NOT NULL DEFAULT '1', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -556,12 +642,12 @@ DROP TABLE IF EXISTS `form_schema_webhooks`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_schema_webhooks` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `trigger_event` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `secret` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `trigger_event` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `url` text COLLATE utf8mb4_unicode_ci NOT NULL, + `secret` text COLLATE utf8mb4_unicode_ci, `is_active` tinyint(1) NOT NULL DEFAULT '1', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -574,34 +660,34 @@ DROP TABLE IF EXISTS `form_schemas`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_schemas` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `owner_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `owner_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `purpose` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `default_crowd_type_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_type` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `purpose` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `default_crowd_type_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` text COLLATE utf8mb4_unicode_ci, `is_published` tinyint(1) NOT NULL DEFAULT '0', - `submission_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `public_token` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `public_token_previous` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `submission_mode` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `public_token` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `public_token_previous` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `public_token_rotated_at` timestamp NULL DEFAULT NULL, `submission_deadline` timestamp NULL DEFAULT NULL, - `locale` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'nl', + `locale` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'nl', `settings` json DEFAULT NULL, `version` int unsigned NOT NULL DEFAULT '1', - `snapshot_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'never', + `snapshot_mode` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'never', `freeze_on_submit` tinyint(1) NOT NULL DEFAULT '0', `retention_days` int unsigned DEFAULT NULL, - `consent_version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `consent_version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `section_level_submit` tinyint(1) NOT NULL DEFAULT '0', `auto_save_enabled` tinyint(1) NOT NULL DEFAULT '0', `max_submissions` int unsigned DEFAULT NULL, - `created_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `last_updated_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `edit_lock_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `last_updated_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `edit_lock_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `edit_lock_expires_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -628,13 +714,13 @@ DROP TABLE IF EXISTS `form_submission_action_failure_retry_attempts`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_submission_action_failure_retry_attempts` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_action_failure_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_action_failure_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `attempted_at` timestamp NOT NULL, - `attempted_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `outcome` enum('succeeded','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `exception_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `exception_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `attempted_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `outcome` enum('succeeded','failed') COLLATE utf8mb4_unicode_ci NOT NULL, + `exception_class` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `exception_message` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -648,23 +734,23 @@ DROP TABLE IF EXISTS `form_submission_action_failures`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_submission_action_failures` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `listener_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `binding_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `listener_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `binding_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `failed_at` timestamp NOT NULL, - `exception_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `exception_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `exception_trace` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `exception_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `exception_message` text COLLATE utf8mb4_unicode_ci NOT NULL, + `exception_trace` longtext COLLATE utf8mb4_unicode_ci, `context` json NOT NULL, `retry_count` tinyint unsigned NOT NULL DEFAULT '0', `resolved_at` timestamp NULL DEFAULT NULL, - `resolved_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `resolved_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `resolved_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `resolved_note` text COLLATE utf8mb4_unicode_ci, `dismissed_at` timestamp NULL DEFAULT NULL, - `dismissed_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `dismissed_reason_type` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `dismissed_reason_note` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dismissed_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dismissed_reason_type` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dismissed_reason_note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -686,13 +772,13 @@ DROP TABLE IF EXISTS `form_submission_delegations`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_submission_delegations` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `delegated_to_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `delegated_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `delegated_to_user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `delegated_by_user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `granted_at` timestamp NOT NULL, `revoked_at` timestamp NULL DEFAULT NULL, - `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `message` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -708,14 +794,14 @@ DROP TABLE IF EXISTS `form_submission_section_statuses`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_submission_section_statuses` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_section_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `status` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_section_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL, `submitted_at` timestamp NULL DEFAULT NULL, - `reviewed_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reviewed_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reviewed_at` timestamp NULL DEFAULT NULL, - `review_notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `review_notes` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -731,39 +817,39 @@ DROP TABLE IF EXISTS `form_submissions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_submissions` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `subject_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `subject_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `submitted_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `public_submitter_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `public_submitter_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `public_submitter_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subject_type` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `subject_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `submitted_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `public_submitter_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `public_submitter_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `public_submitter_ip` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `public_submitter_ip_anonymised_at` timestamp NULL DEFAULT NULL, - `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `review_status` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `reviewed_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `review_status` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reviewed_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reviewed_at` timestamp NULL DEFAULT NULL, - `review_notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `review_notes` text COLLATE utf8mb4_unicode_ci, `submitted_at` timestamp NULL DEFAULT NULL, `schema_version_at_submit` int unsigned DEFAULT NULL, `schema_snapshot` json DEFAULT NULL, `is_test` tinyint(1) NOT NULL DEFAULT '0', - `submitted_in_locale` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `submitted_in_locale` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `opened_at` timestamp NULL DEFAULT NULL, `schema_version_at_open` int unsigned DEFAULT NULL, `first_interacted_at` timestamp NULL DEFAULT NULL, `submission_duration_seconds` int unsigned DEFAULT NULL, `auto_save_count` int unsigned NOT NULL DEFAULT '0', - `idempotency_key` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idempotency_key` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `anonymised_at` timestamp NULL DEFAULT NULL, - `identity_match_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `apply_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `identity_match_status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `apply_status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `failure_response_code` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `apply_completed_at` timestamp NULL DEFAULT NULL, - `search_index` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `search_index` mediumtext COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, @@ -792,12 +878,12 @@ DROP TABLE IF EXISTS `form_templates`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_templates` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `purpose` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `purpose` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci, `schema_snapshot` json NOT NULL, `is_system` tinyint(1) NOT NULL DEFAULT '0', `is_active` tinyint(1) NOT NULL DEFAULT '1', @@ -813,11 +899,11 @@ DROP TABLE IF EXISTS `form_value_options`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_value_options` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_value_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_field_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `option_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_value_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_field_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `option_value` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `fvo_field_option_idx` (`form_field_id`,`option_value`), KEY `fvo_submission_idx` (`form_submission_id`), @@ -831,11 +917,11 @@ DROP TABLE IF EXISTS `form_values`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_values` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_field_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_field_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `value` json NOT NULL, - `value_indexed` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `value_indexed` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `value_number` decimal(15,4) DEFAULT NULL, `value_date` date DEFAULT NULL, `value_bool` tinyint(1) DEFAULT NULL, @@ -855,15 +941,15 @@ DROP TABLE IF EXISTS `form_webhook_deliveries`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `form_webhook_deliveries` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_schema_webhook_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `form_submission_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `trigger_event` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_schema_webhook_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `form_submission_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `trigger_event` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, `attempts` int unsigned NOT NULL DEFAULT '0', `last_attempt_at` timestamp NULL DEFAULT NULL, `response_status` smallint unsigned DEFAULT NULL, - `response_body_excerpt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `response_body_excerpt` text COLLATE utf8mb4_unicode_ci, `next_retry_at` timestamp NULL DEFAULT NULL, `delivered_at` timestamp NULL DEFAULT NULL, `failed_permanently_at` timestamp NULL DEFAULT NULL, @@ -876,21 +962,38 @@ CREATE TABLE `form_webhook_deliveries` ( CONSTRAINT `form_webhook_deliveries_form_submission_id_foreign` FOREIGN KEY (`form_submission_id`) REFERENCES `form_submissions` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `genres`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `genres` ( + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, + `color` varchar(7) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `sort_order` int NOT NULL DEFAULT '0', + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `genres_organisation_id_name_unique` (`organisation_id`,`name`), + CONSTRAINT `genres_organisation_id_foreign` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `impersonation_sessions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `impersonation_sessions` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `admin_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `target_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `mfa_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `admin_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `target_user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `reason` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `mfa_method` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_agent` text COLLATE utf8mb4_unicode_ci, `started_at` timestamp NOT NULL, `ended_at` timestamp NULL DEFAULT NULL, `expires_at` timestamp NOT NULL, - `end_reason` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `end_reason` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `actions_count` int unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `impersonation_sessions_admin_id_ended_at_index` (`admin_id`,`ended_at`), @@ -904,13 +1007,13 @@ DROP TABLE IF EXISTS `job_batches`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `job_batches` ( - `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `total_jobs` int NOT NULL, `pending_jobs` int NOT NULL, `failed_jobs` int NOT NULL, - `failed_job_ids` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `options` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `options` mediumtext COLLATE utf8mb4_unicode_ci, `cancelled_at` int DEFAULT NULL, `created_at` int NOT NULL, `finished_at` int DEFAULT NULL, @@ -922,8 +1025,8 @@ DROP TABLE IF EXISTS `jobs`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `jobs` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `queue` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, `attempts` tinyint unsigned NOT NULL, `reserved_at` int unsigned DEFAULT NULL, `available_at` int unsigned NOT NULL, @@ -936,14 +1039,14 @@ DROP TABLE IF EXISTS `locations`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `locations` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `lat` decimal(10,8) DEFAULT NULL, `lng` decimal(11,8) DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `access_instructions` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `description` text COLLATE utf8mb4_unicode_ci, + `access_instructions` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -955,9 +1058,9 @@ DROP TABLE IF EXISTS `mfa_backup_codes`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mfa_backup_codes` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `code_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `code_hash` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, `used` tinyint(1) NOT NULL DEFAULT '0', `used_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -971,9 +1074,9 @@ DROP TABLE IF EXISTS `mfa_email_codes`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mfa_email_codes` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `code` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `code` varchar(6) COLLATE utf8mb4_unicode_ci NOT NULL, `expires_at` timestamp NOT NULL, `used` tinyint(1) NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT NULL, @@ -988,7 +1091,7 @@ DROP TABLE IF EXISTS `migrations`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `migrations` ( `id` int unsigned NOT NULL AUTO_INCREMENT, - `migration` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `batch` int NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -998,8 +1101,8 @@ DROP TABLE IF EXISTS `model_has_permissions`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `model_has_permissions` ( `permission_id` bigint unsigned NOT NULL, - `model_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `model_id` varchar(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `model_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`permission_id`,`model_id`,`model_type`), KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`), CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE @@ -1010,8 +1113,8 @@ DROP TABLE IF EXISTS `model_has_roles`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `model_has_roles` ( `role_id` bigint unsigned NOT NULL, - `model_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `model_id` varchar(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `model_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`role_id`,`model_id`,`model_type`), KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`), CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE @@ -1021,14 +1124,14 @@ DROP TABLE IF EXISTS `organisation_email_settings`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `organisation_email_settings` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `logo_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `primary_color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#6366F1', - `secondary_color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#4F46E5', - `footer_text` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `reply_to_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `reply_to_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `logo_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `primary_color` varchar(7) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#6366F1', + `secondary_color` varchar(7) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#4F46E5', + `footer_text` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reply_to_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reply_to_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -1040,13 +1143,13 @@ DROP TABLE IF EXISTS `organisation_email_templates`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `organisation_email_templates` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `subject` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `heading` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `body_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `button_text` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `subject` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL, + `heading` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `body_text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `button_text` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -1058,10 +1161,10 @@ DROP TABLE IF EXISTS `organisation_user`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `organisation_user` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `role` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -1075,21 +1178,21 @@ DROP TABLE IF EXISTS `organisations`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `organisations` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `contact_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `contact_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `billing_status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'trial', - `default_locale` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'nl', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `contact_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `contact_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `phone` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `website` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `billing_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'trial', + `default_locale` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'nl', `settings` json DEFAULT NULL, - `email_logo_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `email_primary_color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `email_reply_to` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `email_sender_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `email_footer_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `email_logo_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email_primary_color` varchar(7) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email_reply_to` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email_sender_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email_footer_text` text COLLATE utf8mb4_unicode_ci, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, @@ -1101,19 +1204,44 @@ DROP TABLE IF EXISTS `password_reset_tokens`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `password_reset_tokens` ( - `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `performances`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `performances` ( + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `engagement_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `stage_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `lane` tinyint unsigned NOT NULL DEFAULT '0', + `start_at` datetime NOT NULL, + `end_at` datetime NOT NULL, + `version` int NOT NULL DEFAULT '0', + `notes` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `performances_event_id_stage_id_start_at_end_at_index` (`event_id`,`stage_id`,`start_at`,`end_at`), + KEY `performances_engagement_id_index` (`engagement_id`), + KEY `performances_stage_id_start_at_index` (`stage_id`,`start_at`), + CONSTRAINT `performances_engagement_id_foreign` FOREIGN KEY (`engagement_id`) REFERENCES `artist_engagements` (`id`) ON DELETE CASCADE, + CONSTRAINT `performances_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE, + CONSTRAINT `performances_stage_id_foreign` FOREIGN KEY (`stage_id`) REFERENCES `stages` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `permissions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `permissions` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `guard_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -1124,20 +1252,20 @@ DROP TABLE IF EXISTS `person_identity_matches`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `person_identity_matches` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `matched_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `matched_on` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `confidence` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `matched_user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `matched_on` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `confidence` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', `match_details` json DEFAULT NULL, - `confirmed_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `confirmed_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `confirmed_at` timestamp NULL DEFAULT NULL, - `dismissed_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `dismissed_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `dismissed_at` timestamp NULL DEFAULT NULL, - `reverted_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `reverted_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reverted_at` timestamp NULL DEFAULT NULL, - `resolved_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `resolved_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `resolved_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -1161,9 +1289,9 @@ DROP TABLE IF EXISTS `person_section_preferences`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `person_section_preferences` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `festival_section_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `festival_section_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `priority` tinyint NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `psp_person_section_unique` (`person_id`,`festival_section_id`), @@ -1177,12 +1305,12 @@ DROP TABLE IF EXISTS `person_tags`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `person_tags` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `category` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `color` varchar(7) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `is_active` tinyint(1) NOT NULL DEFAULT '1', `sort_order` int NOT NULL DEFAULT '0', `created_at` timestamp NULL DEFAULT NULL, @@ -1198,11 +1326,11 @@ DROP TABLE IF EXISTS `personal_access_tokens`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `personal_access_tokens` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `tokenable_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `tokenable_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `abilities` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `tokenable_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `abilities` text COLLATE utf8mb4_unicode_ci, `last_used_at` timestamp NULL DEFAULT NULL, `expires_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -1217,21 +1345,21 @@ DROP TABLE IF EXISTS `persons`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `persons` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `crowd_type_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `company_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `first_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `crowd_type_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `company_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `first_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `date_of_birth` date DEFAULT NULL, - `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `status` enum('invited','applied','pending','approved','rejected','no_show') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', - `registration_source` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'organizer', + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` enum('invited','applied','pending','approved','rejected','no_show') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', + `registration_source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'organizer', `is_blacklisted` tinyint(1) NOT NULL DEFAULT '0', - `admin_notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `remarks` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `admin_notes` text COLLATE utf8mb4_unicode_ci, + `remarks` text COLLATE utf8mb4_unicode_ci, `custom_fields` json DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -1265,8 +1393,8 @@ DROP TABLE IF EXISTS `roles`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `roles` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `guard_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), @@ -1277,11 +1405,11 @@ DROP TABLE IF EXISTS `sessions`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `sessions` ( - `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_agent` text COLLATE utf8mb4_unicode_ci, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, `last_activity` int NOT NULL, PRIMARY KEY (`id`), KEY `sessions_user_id_index` (`user_id`), @@ -1292,12 +1420,12 @@ DROP TABLE IF EXISTS `shift_absences`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `shift_absences` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `shift_assignment_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `reason` enum('sick','personal','other') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `shift_assignment_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `reason` enum('sick','personal','other') COLLATE utf8mb4_unicode_ci NOT NULL, `reported_at` timestamp NOT NULL, - `status` enum('open','filled','closed') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open', + `status` enum('open','filled','closed') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open', `closed_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `shift_absences_person_id_foreign` (`person_id`), @@ -1311,19 +1439,19 @@ DROP TABLE IF EXISTS `shift_assignments`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `shift_assignments` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `shift_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `time_slot_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `status` enum('pending_approval','approved','rejected','cancelled','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending_approval', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `shift_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `time_slot_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` enum('pending_approval','approved','rejected','cancelled','completed') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending_approval', `auto_approved` tinyint(1) NOT NULL DEFAULT '0', - `assigned_by` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `assigned_by` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `assigned_at` timestamp NULL DEFAULT NULL, - `approved_by` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `approved_by` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `approved_at` timestamp NULL DEFAULT NULL, - `rejection_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `cancelled_by` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `cancellation_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `rejection_reason` text COLLATE utf8mb4_unicode_ci, + `cancelled_by` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `cancellation_source` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `cancelled_at` timestamp NULL DEFAULT NULL, `hours_expected` decimal(4,2) DEFAULT NULL, `hours_completed` decimal(4,2) DEFAULT NULL, @@ -1352,14 +1480,14 @@ DROP TABLE IF EXISTS `shift_check_ins`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `shift_check_ins` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `shift_assignment_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `shift_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `shift_assignment_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `shift_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `checked_in_at` timestamp NOT NULL, `checked_out_at` timestamp NULL DEFAULT NULL, - `checked_in_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `method` enum('qr','manual') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `checked_in_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `method` enum('qr','manual') COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`), KEY `shift_check_ins_checked_in_by_user_id_foreign` (`checked_in_by_user_id`), KEY `shift_check_ins_shift_assignment_id_index` (`shift_assignment_id`), @@ -1375,12 +1503,12 @@ DROP TABLE IF EXISTS `shift_swap_requests`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `shift_swap_requests` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `from_assignment_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `to_person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `status` enum('pending','accepted','rejected','cancelled','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', - `reviewed_by` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `from_assignment_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `to_person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `message` text COLLATE utf8mb4_unicode_ci, + `status` enum('pending','accepted','rejected','cancelled','completed') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', + `reviewed_by` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reviewed_at` timestamp NULL DEFAULT NULL, `auto_approved` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), @@ -1396,9 +1524,9 @@ DROP TABLE IF EXISTS `shift_waitlist`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `shift_waitlist` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `shift_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `shift_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `position` int unsigned NOT NULL, `added_at` timestamp NOT NULL, `notified_at` timestamp NULL DEFAULT NULL, @@ -1414,15 +1542,15 @@ DROP TABLE IF EXISTS `shifts`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `shifts` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `festival_section_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `time_slot_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `location_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `festival_section_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `time_slot_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `location_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `report_time` time DEFAULT NULL, - `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `instructions` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `coordinator_notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + `instructions` text COLLATE utf8mb4_unicode_ci, + `coordinator_notes` text COLLATE utf8mb4_unicode_ci, `slots_total` int unsigned NOT NULL, `slots_open_for_claiming` int unsigned NOT NULL, `is_lead_role` tinyint(1) NOT NULL DEFAULT '0', @@ -1430,9 +1558,9 @@ CREATE TABLE `shifts` ( `actual_end_time` time DEFAULT NULL, `end_date` date DEFAULT NULL, `allow_overlap` tinyint(1) NOT NULL DEFAULT '0', - `assigned_crew_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `assigned_crew_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `events_during_shift` json DEFAULT NULL, - `status` enum('draft','open','full','in_progress','completed','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'draft', + `status` enum('draft','open','full','in_progress','completed','cancelled') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'draft', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, @@ -1447,17 +1575,49 @@ CREATE TABLE `shifts` ( CONSTRAINT `shifts_time_slot_id_foreign` FOREIGN KEY (`time_slot_id`) REFERENCES `time_slots` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `stage_days`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `stage_days` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `stage_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `stage_days_stage_id_event_id_unique` (`stage_id`,`event_id`), + KEY `stage_days_event_id_index` (`event_id`), + CONSTRAINT `stage_days_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE, + CONSTRAINT `stage_days_stage_id_foreign` FOREIGN KEY (`stage_id`) REFERENCES `stages` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `stages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `stages` ( + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, + `color` varchar(7) COLLATE utf8mb4_unicode_ci NOT NULL, + `capacity` int DEFAULT NULL, + `sort_order` int NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `stages_event_id_name_unique` (`event_id`,`name`), + KEY `stages_event_id_sort_order_index` (`event_id`,`sort_order`), + CONSTRAINT `stages_event_id_foreign` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `telescope_entries`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `telescope_entries` ( `sequence` bigint unsigned NOT NULL AUTO_INCREMENT, - `uuid` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `batch_id` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `family_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `batch_id` char(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `family_hash` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `should_display_on_index` tinyint(1) NOT NULL DEFAULT '1', - `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL, `created_at` datetime DEFAULT NULL, PRIMARY KEY (`sequence`), UNIQUE KEY `telescope_entries_uuid_unique` (`uuid`), @@ -1471,8 +1631,8 @@ DROP TABLE IF EXISTS `telescope_entries_tags`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `telescope_entries_tags` ( - `entry_uuid` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `entry_uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `tag` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`entry_uuid`,`tag`), KEY `telescope_entries_tags_tag_index` (`tag`), CONSTRAINT `telescope_entries_tags_entry_uuid_foreign` FOREIGN KEY (`entry_uuid`) REFERENCES `telescope_entries` (`uuid`) ON DELETE CASCADE @@ -1482,7 +1642,7 @@ DROP TABLE IF EXISTS `telescope_monitoring`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `telescope_monitoring` ( - `tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `tag` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`tag`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1490,10 +1650,10 @@ DROP TABLE IF EXISTS `time_slots`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `time_slots` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_type` enum('CREW','VOLUNTEER','PRESS','PHOTO','PARTNER') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'VOLUNTEER', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_type` enum('CREW','VOLUNTEER','PRESS','PHOTO','PARTNER') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'VOLUNTEER', `date` date NOT NULL, `start_time` time NOT NULL, `end_time` time NOT NULL, @@ -1509,11 +1669,11 @@ DROP TABLE IF EXISTS `trusted_devices`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `trusted_devices` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `device_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `device_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `device_hash` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `device_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL, `trusted_until` timestamp NOT NULL, `last_used_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, @@ -1527,14 +1687,14 @@ DROP TABLE IF EXISTS `user_invitations`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `user_invitations` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `invited_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `event_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `token` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `status` enum('pending','accepted','expired') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `invited_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `event_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `role` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` char(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` enum('pending','accepted','expired') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending', `expires_at` timestamp NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, @@ -1553,14 +1713,14 @@ DROP TABLE IF EXISTS `user_organisation_tags`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `user_organisation_tags` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `organisation_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_tag_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `source` enum('self_reported','organiser_assigned') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `assigned_by_user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `proficiency` enum('beginner','experienced','expert') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `organisation_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_tag_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `source` enum('self_reported','organiser_assigned') COLLATE utf8mb4_unicode_ci NOT NULL, + `assigned_by_user_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `proficiency` enum('beginner','experienced','expert') COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `notes` text COLLATE utf8mb4_unicode_ci, `assigned_at` timestamp NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uot_user_org_tag_source_unique` (`user_id`,`organisation_id`,`person_tag_id`,`source`), @@ -1578,12 +1738,12 @@ DROP TABLE IF EXISTS `user_profiles`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `user_profiles` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `user_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `bio` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - `photo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `emergency_contact_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `emergency_contact_phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `bio` text COLLATE utf8mb4_unicode_ci, + `photo_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `emergency_contact_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `emergency_contact_phone` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `reliability_score` decimal(3,2) NOT NULL DEFAULT '0.00', `is_ambassador` tinyint(1) NOT NULL DEFAULT '0', `settings` json DEFAULT NULL, @@ -1599,23 +1759,23 @@ DROP TABLE IF EXISTS `users`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `users` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `first_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `first_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `date_of_birth` date DEFAULT NULL, - `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `mfa_enabled` tinyint(1) NOT NULL DEFAULT '0', - `mfa_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `mfa_secret` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `mfa_method` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mfa_secret` text COLLATE utf8mb4_unicode_ci, `mfa_confirmed_at` timestamp NULL DEFAULT NULL, `mfa_enforced` tinyint(1) NOT NULL DEFAULT '0', `email_verified_at` timestamp NULL DEFAULT NULL, - `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `timezone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Europe/Amsterdam', - `locale` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'nl', - `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `remember_token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `timezone` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'Europe/Amsterdam', + `locale` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'nl', + `avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, @@ -1627,9 +1787,9 @@ DROP TABLE IF EXISTS `volunteer_availabilities`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `volunteer_availabilities` ( - `id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `person_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `time_slot_id` char(26) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `person_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `time_slot_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, `preference_level` tinyint NOT NULL DEFAULT '3', `submitted_at` timestamp NOT NULL, PRIMARY KEY (`id`), @@ -1671,108 +1831,116 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_04_08_130 INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_04_08_140000_create_shift_assignments_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_04_08_150000_create_shift_check_ins_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_04_08_160000_create_volunteer_availabilities_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_04_08_170000_create_artists_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_04_08_180000_create_advance_sections_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_04_08_200000_create_crowd_types_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_04_08_205712_add_category_and_icon_to_festival_sections_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (27,'2026_04_08_210000_create_companies_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (28,'2026_04_08_220000_create_persons_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (29,'2026_04_08_230000_create_crowd_lists_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (30,'2026_04_08_240000_create_crowd_list_persons_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (31,'2026_04_08_250000_add_person_foreign_keys_to_existing_tables',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (32,'2026_04_08_300000_add_shifts_extras_and_waitlist_tables',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (33,'2026_04_08_310000_drop_unique_person_timeslot_on_shift_assignments',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (34,'2026_04_08_320000_fix_activity_log_morphs_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (35,'2026_04_08_400000_add_festival_columns_to_events_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (36,'2026_04_08_410000_create_event_person_activations_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (37,'2026_04_10_100000_add_registration_branding_to_events_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (38,'2026_04_10_100000_create_person_tags_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (39,'2026_04_10_110000_create_user_organisation_tags_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (40,'2026_04_10_193837_add_cancellation_tracking_to_shift_assignments',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (41,'2026_04_10_200000_create_person_identity_matches_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (42,'2026_04_10_300000_add_registration_fields_to_festival_sections_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (43,'2026_04_10_400000_split_name_into_first_last_on_users_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (44,'2026_04_10_400001_split_name_into_first_last_on_persons_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (45,'2026_04_10_400002_split_contact_name_on_companies_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (46,'2026_04_10_500000_add_date_of_birth_to_persons_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (47,'2026_04_12_100000_add_remarks_to_persons_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (48,'2026_04_12_100001_add_registration_toggles_to_events_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (49,'2026_04_12_200000_create_registration_field_templates_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (50,'2026_04_12_200001_create_registration_form_fields_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (51,'2026_04_12_200002_create_person_field_values_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (52,'2026_04_12_200003_create_person_section_preferences_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (53,'2026_04_13_100000_add_email_branding_to_organisations_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (54,'2026_04_14_100000_update_token_columns_for_hashed_storage',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (55,'2026_04_14_200000_add_date_of_birth_to_users_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (56,'2026_04_14_200001_enhance_person_identity_matches_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (57,'2026_04_14_300000_create_email_change_requests_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (58,'2026_04_15_100000_create_organisation_email_settings_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (59,'2026_04_15_100001_create_organisation_email_templates_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (60,'2026_04_15_100002_create_email_logs_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (61,'2026_04_15_200000_add_mfa_columns_to_users_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (62,'2026_04_15_200001_create_mfa_backup_codes_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (63,'2026_04_15_200002_create_trusted_devices_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (64,'2026_04_15_200003_create_mfa_email_codes_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (65,'2026_04_16_000000_add_phone_to_users_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (66,'2026_04_16_100000_create_impersonation_sessions_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (67,'2026_04_16_142626_remove_section_from_registration_fields_tables',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (68,'2026_04_17_100000_add_registration_source_to_persons_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (69,'2026_04_17_200000_add_display_width_to_registration_fields_tables',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (70,'2026_04_18_100000_change_tag_category_to_tag_categories_on_registration_fields',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (71,'2026_04_18_100001_update_partner_crowd_type_icon',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (72,'2026_04_18_110000_add_contact_fields_to_organisations_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (73,'2026_04_19_100000_create_user_profiles_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (74,'2026_04_19_100001_populate_user_profiles_from_existing_users',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (75,'2026_04_19_100002_create_form_schemas_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (76,'2026_04_19_100003_create_form_schema_sections_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (77,'2026_04_19_100004_create_form_field_library_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (78,'2026_04_19_100005_create_form_fields_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (79,'2026_04_19_100006_create_form_submissions_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (80,'2026_04_19_100007_create_form_submission_section_statuses_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (81,'2026_04_19_100008_create_form_submission_delegations_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (82,'2026_04_19_100009_create_form_values_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (83,'2026_04_19_100010_create_form_value_options_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (84,'2026_04_19_100011_create_form_templates_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (85,'2026_04_19_100012_create_form_schema_webhooks_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (86,'2026_04_19_100013_create_form_webhook_deliveries_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (87,'2026_04_20_100000_drop_remaining_legacy_registration_tables',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (88,'2026_04_21_100000_add_default_locale_to_organisations_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (89,'2026_04_22_100000_add_identity_match_status_to_form_submissions',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (90,'2026_04_22_100001_add_idempotency_key_unique_to_form_submissions',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (91,'2026_04_22_100002_add_schema_version_at_open_to_form_submissions',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (92,'2026_04_24_100000_purge_invalid_form_purposes',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (93,'2026_04_24_100001_drop_custom_purpose_slug_from_form_schemas',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (94,'2026_04_24_110001_migrate_organisation_user_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (95,'2026_04_24_110002_migrate_event_user_roles_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (96,'2026_04_24_110003_migrate_crowd_list_persons_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (97,'2026_04_24_110004_migrate_event_person_activations_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (98,'2026_04_24_110005_migrate_user_organisation_tags_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (99,'2026_04_24_110006_migrate_person_section_preferences_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (100,'2026_04_24_110007_migrate_mfa_backup_codes_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (101,'2026_04_24_110008_migrate_mfa_email_codes_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (102,'2026_04_24_110009_migrate_form_submission_section_statuses_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (103,'2026_04_24_110010_migrate_form_values_and_options_to_ulid',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (104,'2026_04_24_200000_add_denormalized_context_to_form_submissions',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (105,'2026_04_25_015838_create_telescope_entries_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (106,'2026_04_25_100000_create_form_field_bindings_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (107,'2026_04_25_100001_drop_binding_json_columns',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (108,'2026_04_25_110000_create_form_field_validation_rules_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (109,'2026_04_25_110001_backfill_form_field_validation_rules',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (110,'2026_04_25_120000_create_form_field_configs_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (111,'2026_04_25_120001_backfill_form_field_configs',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (112,'2026_04_25_120002_drop_validation_rules_json_columns',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (113,'2026_04_25_140000_extend_form_submissions_with_apply_status',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (114,'2026_04_25_140100_create_form_submission_action_failures',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (115,'2026_04_26_100000_create_form_field_conditional_logic_groups_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (116,'2026_04_26_100001_create_form_field_conditional_logic_conditions_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (117,'2026_04_26_100002_backfill_form_field_conditional_logic',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (118,'2026_04_26_100003_drop_conditional_logic_json_column',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (119,'2026_04_26_120000_add_default_crowd_type_id_to_form_schemas',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (120,'2026_04_27_100000_create_form_field_options_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (121,'2026_04_27_100001_backfill_form_field_options',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (122,'2026_04_27_100002_drop_form_field_options_json_columns',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (123,'2026_04_28_100000_restore_default_crowd_type_id_foreign_key',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (124,'2026_04_28_140000_add_kvk_number_to_companies_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (125,'2026_04_28_180000_create_form_submission_action_failure_retry_attempts_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (126,'2026_04_28_181000_add_exception_trace_to_form_submission_action_failures',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (127,'2026_05_08_000001_add_failure_response_code_to_form_submissions',3); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_04_08_200000_create_crowd_types_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_04_08_205712_add_category_and_icon_to_festival_sections_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_04_08_210000_create_companies_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_04_08_220000_create_persons_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (27,'2026_04_08_230000_create_crowd_lists_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (28,'2026_04_08_240000_create_crowd_list_persons_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (29,'2026_04_08_250000_add_person_foreign_keys_to_existing_tables',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (30,'2026_04_08_300000_add_shifts_extras_and_waitlist_tables',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (31,'2026_04_08_310000_drop_unique_person_timeslot_on_shift_assignments',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (32,'2026_04_08_320000_fix_activity_log_morphs_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (33,'2026_04_08_400000_add_festival_columns_to_events_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (34,'2026_04_08_410000_create_event_person_activations_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (35,'2026_04_10_100000_add_registration_branding_to_events_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (36,'2026_04_10_100000_create_person_tags_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (37,'2026_04_10_110000_create_user_organisation_tags_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (38,'2026_04_10_193837_add_cancellation_tracking_to_shift_assignments',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (39,'2026_04_10_200000_create_person_identity_matches_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (40,'2026_04_10_300000_add_registration_fields_to_festival_sections_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (41,'2026_04_10_400000_split_name_into_first_last_on_users_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (42,'2026_04_10_400001_split_name_into_first_last_on_persons_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (43,'2026_04_10_400002_split_contact_name_on_companies_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (44,'2026_04_10_500000_add_date_of_birth_to_persons_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (45,'2026_04_12_100000_add_remarks_to_persons_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (46,'2026_04_12_100001_add_registration_toggles_to_events_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (47,'2026_04_12_200000_create_registration_field_templates_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (48,'2026_04_12_200001_create_registration_form_fields_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (49,'2026_04_12_200002_create_person_field_values_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (50,'2026_04_12_200003_create_person_section_preferences_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (51,'2026_04_13_100000_add_email_branding_to_organisations_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (52,'2026_04_14_100000_update_token_columns_for_hashed_storage',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (53,'2026_04_14_200000_add_date_of_birth_to_users_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (54,'2026_04_14_200001_enhance_person_identity_matches_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (55,'2026_04_14_300000_create_email_change_requests_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (56,'2026_04_15_100000_create_organisation_email_settings_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (57,'2026_04_15_100001_create_organisation_email_templates_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (58,'2026_04_15_100002_create_email_logs_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (59,'2026_04_15_200000_add_mfa_columns_to_users_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (60,'2026_04_15_200001_create_mfa_backup_codes_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (61,'2026_04_15_200002_create_trusted_devices_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (62,'2026_04_15_200003_create_mfa_email_codes_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (63,'2026_04_16_000000_add_phone_to_users_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (64,'2026_04_16_100000_create_impersonation_sessions_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (65,'2026_04_16_142626_remove_section_from_registration_fields_tables',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (66,'2026_04_17_100000_add_registration_source_to_persons_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (67,'2026_04_17_200000_add_display_width_to_registration_fields_tables',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (68,'2026_04_18_100000_change_tag_category_to_tag_categories_on_registration_fields',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (69,'2026_04_18_100001_update_partner_crowd_type_icon',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (70,'2026_04_18_110000_add_contact_fields_to_organisations_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (71,'2026_04_19_100000_create_user_profiles_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (72,'2026_04_19_100001_populate_user_profiles_from_existing_users',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (73,'2026_04_19_100002_create_form_schemas_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (74,'2026_04_19_100003_create_form_schema_sections_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (75,'2026_04_19_100004_create_form_field_library_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (76,'2026_04_19_100005_create_form_fields_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (77,'2026_04_19_100006_create_form_submissions_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (78,'2026_04_19_100007_create_form_submission_section_statuses_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (79,'2026_04_19_100008_create_form_submission_delegations_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (80,'2026_04_19_100009_create_form_values_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (81,'2026_04_19_100010_create_form_value_options_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (82,'2026_04_19_100011_create_form_templates_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (83,'2026_04_19_100012_create_form_schema_webhooks_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (84,'2026_04_19_100013_create_form_webhook_deliveries_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (85,'2026_04_20_100000_drop_remaining_legacy_registration_tables',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (86,'2026_04_21_100000_add_default_locale_to_organisations_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (87,'2026_04_22_100000_add_identity_match_status_to_form_submissions',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (88,'2026_04_22_100001_add_idempotency_key_unique_to_form_submissions',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (89,'2026_04_22_100002_add_schema_version_at_open_to_form_submissions',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (90,'2026_04_24_100000_purge_invalid_form_purposes',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (91,'2026_04_24_100001_drop_custom_purpose_slug_from_form_schemas',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (92,'2026_04_24_110001_migrate_organisation_user_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (93,'2026_04_24_110002_migrate_event_user_roles_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (94,'2026_04_24_110003_migrate_crowd_list_persons_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (95,'2026_04_24_110004_migrate_event_person_activations_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (96,'2026_04_24_110005_migrate_user_organisation_tags_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (97,'2026_04_24_110006_migrate_person_section_preferences_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (98,'2026_04_24_110007_migrate_mfa_backup_codes_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (99,'2026_04_24_110008_migrate_mfa_email_codes_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (100,'2026_04_24_110009_migrate_form_submission_section_statuses_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (101,'2026_04_24_110010_migrate_form_values_and_options_to_ulid',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (102,'2026_04_24_200000_add_denormalized_context_to_form_submissions',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (103,'2026_04_25_015838_create_telescope_entries_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (104,'2026_04_25_100000_create_form_field_bindings_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (105,'2026_04_25_100001_drop_binding_json_columns',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (106,'2026_04_25_110000_create_form_field_validation_rules_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (107,'2026_04_25_110001_backfill_form_field_validation_rules',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (108,'2026_04_25_120000_create_form_field_configs_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (109,'2026_04_25_120001_backfill_form_field_configs',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (110,'2026_04_25_120002_drop_validation_rules_json_columns',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (111,'2026_04_25_140000_extend_form_submissions_with_apply_status',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (112,'2026_04_25_140100_create_form_submission_action_failures',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (113,'2026_04_26_100000_create_form_field_conditional_logic_groups_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (114,'2026_04_26_100001_create_form_field_conditional_logic_conditions_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (115,'2026_04_26_100002_backfill_form_field_conditional_logic',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (116,'2026_04_26_100003_drop_conditional_logic_json_column',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (117,'2026_04_26_120000_add_default_crowd_type_id_to_form_schemas',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (118,'2026_04_27_100000_create_form_field_options_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (119,'2026_04_27_100001_backfill_form_field_options',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (120,'2026_04_27_100002_drop_form_field_options_json_columns',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (121,'2026_04_28_100000_restore_default_crowd_type_id_foreign_key',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (122,'2026_04_28_140000_add_kvk_number_to_companies_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (123,'2026_04_28_180000_create_form_submission_action_failure_retry_attempts_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (124,'2026_04_28_181000_add_exception_trace_to_form_submission_action_failures',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (125,'2026_05_08_000001_add_failure_response_code_to_form_submissions',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (136,'2026_05_08_100000_create_genres_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (137,'2026_05_08_100001_create_artists_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (138,'2026_05_08_100002_add_handles_buma_to_companies_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (139,'2026_05_08_100003_create_artist_contacts_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (140,'2026_05_08_100004_create_stages_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (141,'2026_05_08_100005_create_stage_days_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (142,'2026_05_08_100006_create_artist_engagements_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (143,'2026_05_08_100007_create_performances_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (144,'2026_05_08_100008_create_advance_sections_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (145,'2026_05_08_100009_create_advance_submissions_table',2); -- 2.39.5 From 9ccf1eacebf5c086cbe4ef8b6b1f85bf86a5f116 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:00:28 +0200 Subject: [PATCH 03/14] =?UTF-8?q?feat(timetable):=20Artist=20domain=20?= =?UTF-8?q?=E2=80=94=207=20enums=20+=209=20Eloquent=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Artist/AdvanceSectionSubmissionStatus.php | 20 +++ api/app/Enums/Artist/AdvanceSectionType.php | 20 +++ .../Enums/Artist/AdvanceSubmissionStatus.php | 17 +++ .../Enums/Artist/ArtistEngagementStatus.php | 39 ++++++ api/app/Enums/Artist/BumaHandledBy.php | 26 ++++ api/app/Enums/Artist/FeeType.php | 26 ++++ api/app/Enums/Artist/PaymentStatus.php | 26 ++++ api/app/Models/AdvanceSection.php | 79 +++++++++++ api/app/Models/AdvanceSubmission.php | 70 ++++++++++ api/app/Models/Artist.php | 107 +++++++++++++++ api/app/Models/ArtistContact.php | 69 ++++++++++ api/app/Models/ArtistEngagement.php | 123 ++++++++++++++++++ api/app/Models/Company.php | 13 ++ api/app/Models/Event.php | 17 ++- api/app/Models/Genre.php | 59 +++++++++ api/app/Models/Organisation.php | 15 +++ api/app/Models/Performance.php | 81 ++++++++++++ api/app/Models/Stage.php | 76 +++++++++++ api/app/Models/StageDay.php | 51 ++++++++ 19 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php create mode 100644 api/app/Enums/Artist/AdvanceSectionType.php create mode 100644 api/app/Enums/Artist/AdvanceSubmissionStatus.php create mode 100644 api/app/Enums/Artist/ArtistEngagementStatus.php create mode 100644 api/app/Enums/Artist/BumaHandledBy.php create mode 100644 api/app/Enums/Artist/FeeType.php create mode 100644 api/app/Enums/Artist/PaymentStatus.php create mode 100644 api/app/Models/AdvanceSection.php create mode 100644 api/app/Models/AdvanceSubmission.php create mode 100644 api/app/Models/Artist.php create mode 100644 api/app/Models/ArtistContact.php create mode 100644 api/app/Models/ArtistEngagement.php create mode 100644 api/app/Models/Genre.php create mode 100644 api/app/Models/Performance.php create mode 100644 api/app/Models/Stage.php create mode 100644 api/app/Models/StageDay.php diff --git a/api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php b/api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php new file mode 100644 index 00000000..7e69606e --- /dev/null +++ b/api/app/Enums/Artist/AdvanceSectionSubmissionStatus.php @@ -0,0 +1,20 @@ + 'Concept', + self::Requested => 'Aangevraagd', + self::Option => 'Optie', + self::Offered => 'Aanbod uit', + self::Confirmed => 'Bevestigd', + self::Contracted => 'Gecontracteerd', + self::Cancelled => 'Geannuleerd', + self::Rejected => 'Afgewezen', + self::Declined => 'Bedankt', + }; + } +} diff --git a/api/app/Enums/Artist/BumaHandledBy.php b/api/app/Enums/Artist/BumaHandledBy.php new file mode 100644 index 00000000..0a8e79d1 --- /dev/null +++ b/api/app/Enums/Artist/BumaHandledBy.php @@ -0,0 +1,26 @@ + 'Organisatie', + self::BookingAgency => 'Boekingsagent', + self::NotApplicable => 'Niet van toepassing', + }; + } +} diff --git a/api/app/Enums/Artist/FeeType.php b/api/app/Enums/Artist/FeeType.php new file mode 100644 index 00000000..8db3dea2 --- /dev/null +++ b/api/app/Enums/Artist/FeeType.php @@ -0,0 +1,26 @@ + 'Vaste fee', + self::DoorSplit => 'Door split', + self::GuaranteePlusSplit => 'Garantie + split', + }; + } +} diff --git a/api/app/Enums/Artist/PaymentStatus.php b/api/app/Enums/Artist/PaymentStatus.php new file mode 100644 index 00000000..6abad00f --- /dev/null +++ b/api/app/Enums/Artist/PaymentStatus.php @@ -0,0 +1,26 @@ + 'Geen betaling', + self::DepositPaid => 'Aanbetaling voldaan', + self::PaidInFull => 'Volledig voldaan', + }; + } +} diff --git a/api/app/Models/AdvanceSection.php b/api/app/Models/AdvanceSection.php new file mode 100644 index 00000000..a41b1bc1 --- /dev/null +++ b/api/app/Models/AdvanceSection.php @@ -0,0 +1,79 @@ + ArtistEngagement::class, 'fk' => 'engagement_id']; + } + + protected $fillable = [ + 'engagement_id', + 'name', + 'type', + 'is_open', + 'open_from', + 'open_to', + 'sort_order', + 'submission_status', + 'last_submitted_at', + 'last_submitted_by', + 'submission_diff', + ]; + + protected function casts(): array + { + return [ + 'type' => AdvanceSectionType::class, + 'submission_status' => AdvanceSectionSubmissionStatus::class, + 'is_open' => 'boolean', + 'open_from' => 'datetime', + 'open_to' => 'datetime', + 'sort_order' => 'integer', + 'last_submitted_at' => 'datetime', + 'submission_diff' => 'array', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function engagement(): BelongsTo + { + return $this->belongsTo(ArtistEngagement::class, 'engagement_id'); + } + + public function submissions(): HasMany + { + return $this->hasMany(AdvanceSubmission::class); + } +} diff --git a/api/app/Models/AdvanceSubmission.php b/api/app/Models/AdvanceSubmission.php new file mode 100644 index 00000000..cefe1a9b --- /dev/null +++ b/api/app/Models/AdvanceSubmission.php @@ -0,0 +1,70 @@ + AdvanceSection::class, 'fk' => 'advance_section_id']; + } + + protected $fillable = [ + 'advance_section_id', + 'submitted_by_name', + 'submitted_by_email', + 'submitted_at', + 'status', + 'reviewed_by', + 'reviewed_at', + 'data', + ]; + + protected function casts(): array + { + return [ + 'status' => AdvanceSubmissionStatus::class, + 'submitted_at' => 'datetime', + 'reviewed_at' => 'datetime', + 'data' => 'array', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function section(): BelongsTo + { + return $this->belongsTo(AdvanceSection::class, 'advance_section_id'); + } + + public function reviewer(): BelongsTo + { + return $this->belongsTo(User::class, 'reviewed_by'); + } +} diff --git a/api/app/Models/Artist.php b/api/app/Models/Artist.php new file mode 100644 index 00000000..783cc5b1 --- /dev/null +++ b/api/app/Models/Artist.php @@ -0,0 +1,107 @@ +slug)) { + $artist->slug = $artist->generateUniqueSlug($artist->name); + } + }); + } + + protected $fillable = [ + 'organisation_id', + 'name', + 'slug', + 'default_genre_id', + 'default_draw', + 'star_rating', + 'home_base_country', + 'agent_company_id', + 'notes', + ]; + + protected function casts(): array + { + return [ + 'default_draw' => 'integer', + 'star_rating' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + private function generateUniqueSlug(string $name): string + { + $base = Str::slug($name); + $slug = $base; + $suffix = 2; + + while ( + self::withoutGlobalScope(OrganisationScope::class) + ->withTrashed() + ->where('organisation_id', $this->organisation_id) + ->where('slug', $slug) + ->exists() + ) { + $slug = "{$base}-{$suffix}"; + $suffix++; + } + + return $slug; + } + + public function organisation(): BelongsTo + { + return $this->belongsTo(Organisation::class); + } + + public function defaultGenre(): BelongsTo + { + return $this->belongsTo(Genre::class, 'default_genre_id'); + } + + public function agentCompany(): BelongsTo + { + return $this->belongsTo(Company::class, 'agent_company_id'); + } + + public function contacts(): HasMany + { + return $this->hasMany(ArtistContact::class); + } + + public function engagements(): HasMany + { + return $this->hasMany(ArtistEngagement::class); + } +} diff --git a/api/app/Models/ArtistContact.php b/api/app/Models/ArtistContact.php new file mode 100644 index 00000000..8a4a0382 --- /dev/null +++ b/api/app/Models/ArtistContact.php @@ -0,0 +1,69 @@ + Artist::class, 'fk' => 'artist_id']; + } + + protected $fillable = [ + 'artist_id', + 'name', + 'email', + 'phone', + 'role', + 'is_primary', + 'receives_briefing', + 'receives_infosheet', + ]; + + protected function casts(): array + { + return [ + 'is_primary' => 'boolean', + 'receives_briefing' => 'boolean', + 'receives_infosheet' => 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function artist(): BelongsTo + { + return $this->belongsTo(Artist::class); + } + + public function scopePrimary(Builder $query): Builder + { + return $query->where('is_primary', true); + } +} diff --git a/api/app/Models/ArtistEngagement.php b/api/app/Models/ArtistEngagement.php new file mode 100644 index 00000000..dbb58428 --- /dev/null +++ b/api/app/Models/ArtistEngagement.php @@ -0,0 +1,123 @@ + ArtistEngagementStatus::class, + 'fee_type' => FeeType::class, + 'buma_handled_by' => BumaHandledBy::class, + 'payment_status' => PaymentStatus::class, + 'buma_applicable' => 'boolean', + 'vat_applicable' => 'boolean', + 'deal_breakdown' => 'array', + 'deposit_due_date' => 'date', + 'balance_due_date' => 'date', + 'crew_count' => 'integer', + 'guests_count' => 'integer', + 'requested_at' => 'datetime', + 'option_expires_at' => 'datetime', + 'advance_open_from' => 'datetime', + 'advance_open_to' => 'datetime', + 'advancing_completed_count' => 'integer', + 'advancing_total_count' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function organisation(): BelongsTo + { + return $this->belongsTo(Organisation::class); + } + + public function artist(): BelongsTo + { + return $this->belongsTo(Artist::class); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + public function projectLeader(): BelongsTo + { + return $this->belongsTo(User::class, 'project_leader_id'); + } + + public function performances(): HasMany + { + return $this->hasMany(Performance::class, 'engagement_id'); + } + + public function advanceSections(): HasMany + { + return $this->hasMany(AdvanceSection::class, 'engagement_id'); + } +} diff --git a/api/app/Models/Company.php b/api/app/Models/Company.php index 06b1bf0f..1899fc39 100644 --- a/api/app/Models/Company.php +++ b/api/app/Models/Company.php @@ -28,6 +28,7 @@ final class Company extends Model 'organisation_id', 'name', 'type', + 'handles_buma', 'kvk_number', 'contact_first_name', 'contact_last_name', @@ -35,6 +36,13 @@ final class Company extends Model 'contact_phone', ]; + protected function casts(): array + { + return [ + 'handles_buma' => 'boolean', + ]; + } + public function getContactFullNameAttribute(): ?string { if (! $this->contact_first_name) { @@ -54,6 +62,11 @@ final class Company extends Model return $this->hasMany(Person::class); } + public function artistsAsAgent(): HasMany + { + return $this->hasMany(Artist::class, 'agent_company_id'); + } + /** @param Builder $query */ public function scopeOrdered(Builder $query): Builder { diff --git a/api/app/Models/Event.php b/api/app/Models/Event.php index 7c1ab402..c0d4f57d 100644 --- a/api/app/Models/Event.php +++ b/api/app/Models/Event.php @@ -27,7 +27,7 @@ final class Event extends Model protected static function booted(): void { - static::addGlobalScope(new OrganisationScope()); + self::addGlobalScope(new OrganisationScope); } /** @var array> Allowed status transitions */ @@ -153,6 +153,21 @@ final class Event extends Model ->orderBy('name'); } + public function stages(): HasMany + { + return $this->hasMany(Stage::class); + } + + public function artistEngagements(): HasMany + { + return $this->hasMany(ArtistEngagement::class); + } + + public function performances(): HasMany + { + return $this->hasMany(Performance::class); + } + // ----- Status State Machine ----- public function canTransitionTo(string $newStatus): bool diff --git a/api/app/Models/Genre.php b/api/app/Models/Genre.php new file mode 100644 index 00000000..070f8e09 --- /dev/null +++ b/api/app/Models/Genre.php @@ -0,0 +1,59 @@ + 'integer', + 'is_active' => 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function organisation(): BelongsTo + { + return $this->belongsTo(Organisation::class); + } + + public function artists(): HasMany + { + return $this->hasMany(Artist::class, 'default_genre_id'); + } +} diff --git a/api/app/Models/Organisation.php b/api/app/Models/Organisation.php index 4b5c9cb1..04704fe9 100644 --- a/api/app/Models/Organisation.php +++ b/api/app/Models/Organisation.php @@ -100,4 +100,19 @@ final class Organisation extends Model { return $this->hasMany(EmailLog::class); } + + public function artists(): HasMany + { + return $this->hasMany(Artist::class); + } + + public function genres(): HasMany + { + return $this->hasMany(Genre::class); + } + + public function artistEngagements(): HasMany + { + return $this->hasMany(ArtistEngagement::class); + } } diff --git a/api/app/Models/Performance.php b/api/app/Models/Performance.php new file mode 100644 index 00000000..639775e2 --- /dev/null +++ b/api/app/Models/Performance.php @@ -0,0 +1,81 @@ + ArtistEngagement::class, 'fk' => 'engagement_id']; + } + + protected $fillable = [ + 'engagement_id', + 'event_id', + 'stage_id', + 'lane', + 'start_at', + 'end_at', + 'version', + 'notes', + ]; + + protected function casts(): array + { + return [ + 'lane' => 'integer', + 'start_at' => 'datetime', + 'end_at' => 'datetime', + 'version' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function engagement(): BelongsTo + { + return $this->belongsTo(ArtistEngagement::class, 'engagement_id'); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + public function stage(): BelongsTo + { + return $this->belongsTo(Stage::class); + } + + public function isParked(): bool + { + return $this->stage_id === null; + } +} diff --git a/api/app/Models/Stage.php b/api/app/Models/Stage.php new file mode 100644 index 00000000..7bd60a68 --- /dev/null +++ b/api/app/Models/Stage.php @@ -0,0 +1,76 @@ + Event::class, 'fk' => 'event_id']; + } + + protected $fillable = [ + 'event_id', + 'name', + 'color', + 'capacity', + 'sort_order', + ]; + + protected function casts(): array + { + return [ + 'capacity' => 'integer', + 'sort_order' => 'integer', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logFillable() + ->dontSubmitEmptyLogs(); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } + + public function stageDays(): HasMany + { + return $this->hasMany(StageDay::class); + } + + public function performances(): HasMany + { + return $this->hasMany(Performance::class); + } + + public function scopeOrdered(Builder $query): Builder + { + return $query->orderBy('sort_order')->orderBy('created_at'); + } +} diff --git a/api/app/Models/StageDay.php b/api/app/Models/StageDay.php new file mode 100644 index 00000000..0619f372 --- /dev/null +++ b/api/app/Models/StageDay.php @@ -0,0 +1,51 @@ + Stage::class, 'fk' => 'stage_id']; + } + + protected $fillable = [ + 'stage_id', + 'event_id', + ]; + + public function stage(): BelongsTo + { + return $this->belongsTo(Stage::class); + } + + public function event(): BelongsTo + { + return $this->belongsTo(Event::class); + } +} -- 2.39.5 From 85ad45c7e971b63a89f0a1274823139f436d0b25 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:01:42 +0200 Subject: [PATCH 04/14] =?UTF-8?q?feat(timetable):=20observers=20=E2=80=94?= =?UTF-8?q?=20engagement=20denorm/guard=20+=20performance=20version=20bump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Artist/CrossTenantEngagementException.php | 29 +++++++++ .../Observers/ArtistEngagementObserver.php | 61 +++++++++++++++++++ api/app/Observers/PerformanceObserver.php | 25 ++++++++ api/app/Providers/AppServiceProvider.php | 5 ++ 4 files changed, 120 insertions(+) create mode 100644 api/app/Exceptions/Artist/CrossTenantEngagementException.php create mode 100644 api/app/Observers/ArtistEngagementObserver.php create mode 100644 api/app/Observers/PerformanceObserver.php diff --git a/api/app/Exceptions/Artist/CrossTenantEngagementException.php b/api/app/Exceptions/Artist/CrossTenantEngagementException.php new file mode 100644 index 00000000..bf98e3ff --- /dev/null +++ b/api/app/Exceptions/Artist/CrossTenantEngagementException.php @@ -0,0 +1,29 @@ +artist_id ?? 'null', + $engagement->artist?->organisation_id ?? 'null', + $engagement->event_id ?? 'null', + $engagement->event?->organisation_id ?? 'null', + )); + } +} diff --git a/api/app/Observers/ArtistEngagementObserver.php b/api/app/Observers/ArtistEngagementObserver.php new file mode 100644 index 00000000..43a3995d --- /dev/null +++ b/api/app/Observers/ArtistEngagementObserver.php @@ -0,0 +1,61 @@ +artist()->withoutGlobalScope(OrganisationScope::class)->first(); + $event = $engagement->event()->withoutGlobalScope(OrganisationScope::class)->first(); + + if ($artist === null || $event === null) { + return; + } + + if ($engagement->organisation_id === null) { + $engagement->organisation_id = $artist->organisation_id; + } + + if ($artist->organisation_id !== $event->organisation_id) { + $engagement->setRelation('artist', $artist); + $engagement->setRelation('event', $event); + throw CrossTenantEngagementException::forEngagement($engagement); + } + } + + /** + * Reserved for Session 2 state-machine validation when + * `booking_status` transitions land. No-op for now. + */ + public function saving(ArtistEngagement $engagement): void + { + // intentionally empty — see class docblock + } + + public function deleted(ArtistEngagement $engagement): void + { + if (! $engagement->isForceDeleting() && $engagement->trashed()) { + $engagement->performances()->delete(); + $engagement->advanceSections()->delete(); + } + } +} diff --git a/api/app/Observers/PerformanceObserver.php b/api/app/Observers/PerformanceObserver.php new file mode 100644 index 00000000..cc3249d1 --- /dev/null +++ b/api/app/Observers/PerformanceObserver.php @@ -0,0 +1,25 @@ +exists && $performance->isDirty()) { + $performance->version = (int) $performance->version + 1; + } + } +} diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php index a0718ada..0aaf16ad 100644 --- a/api/app/Providers/AppServiceProvider.php +++ b/api/app/Providers/AppServiceProvider.php @@ -163,6 +163,11 @@ class AppServiceProvider extends ServiceProvider FormField::observe(FormFieldChildTablesCascadeObserver::class); FormFieldLibrary::observe(FormFieldChildTablesCascadeObserver::class); + // RFC-TIMETABLE v0.2 — engagement denorm + cross-tenant guard, + // performance optimistic-lock bump. + \App\Models\ArtistEngagement::observe(\App\Observers\ArtistEngagementObserver::class); + \App\Models\Performance::observe(\App\Observers\PerformanceObserver::class); + // RFC-WS-6 v1.3 §Q1 — FormSubmissionSubmitted listener layout. // // SYNC chain (single listener): -- 2.39.5 From 3e3636dc5303b16fb8361a267406e18ff1bec0ab Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:08:16 +0200 Subject: [PATCH 05/14] feat(timetable): factories + ArtistTimetableDevSeeder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- api/app/Models/AdvanceSection.php | 2 +- api/app/Models/AdvanceSubmission.php | 2 +- api/app/Models/Artist.php | 2 +- api/app/Models/ArtistContact.php | 2 +- api/app/Models/ArtistEngagement.php | 2 +- api/app/Models/Genre.php | 2 +- api/app/Models/Performance.php | 2 +- api/app/Models/Stage.php | 2 +- .../factories/AdvanceSectionFactory.php | 46 ++++ .../factories/AdvanceSubmissionFactory.php | 27 +++ .../factories/ArtistContactFactory.php | 42 ++++ .../factories/ArtistEngagementFactory.php | 84 +++++++ api/database/factories/ArtistFactory.php | 57 +++++ api/database/factories/GenreFactory.php | 25 +++ api/database/factories/PerformanceFactory.php | 48 ++++ api/database/factories/StageFactory.php | 30 +++ .../seeders/ArtistTimetableDevSeeder.php | 209 ++++++++++++++++++ api/database/seeders/DevSeeder.php | 15 +- 18 files changed, 588 insertions(+), 11 deletions(-) create mode 100644 api/database/factories/AdvanceSectionFactory.php create mode 100644 api/database/factories/AdvanceSubmissionFactory.php create mode 100644 api/database/factories/ArtistContactFactory.php create mode 100644 api/database/factories/ArtistEngagementFactory.php create mode 100644 api/database/factories/ArtistFactory.php create mode 100644 api/database/factories/GenreFactory.php create mode 100644 api/database/factories/PerformanceFactory.php create mode 100644 api/database/factories/StageFactory.php create mode 100644 api/database/seeders/ArtistTimetableDevSeeder.php diff --git a/api/app/Models/AdvanceSection.php b/api/app/Models/AdvanceSection.php index a41b1bc1..e2961a2d 100644 --- a/api/app/Models/AdvanceSection.php +++ b/api/app/Models/AdvanceSection.php @@ -64,7 +64,7 @@ final class AdvanceSection extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function engagement(): BelongsTo diff --git a/api/app/Models/AdvanceSubmission.php b/api/app/Models/AdvanceSubmission.php index cefe1a9b..0afdbbef 100644 --- a/api/app/Models/AdvanceSubmission.php +++ b/api/app/Models/AdvanceSubmission.php @@ -55,7 +55,7 @@ final class AdvanceSubmission extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function section(): BelongsTo diff --git a/api/app/Models/Artist.php b/api/app/Models/Artist.php index 783cc5b1..f16e31a2 100644 --- a/api/app/Models/Artist.php +++ b/api/app/Models/Artist.php @@ -57,7 +57,7 @@ final class Artist extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } private function generateUniqueSlug(string $name): string diff --git a/api/app/Models/ArtistContact.php b/api/app/Models/ArtistContact.php index 8a4a0382..284ac4c9 100644 --- a/api/app/Models/ArtistContact.php +++ b/api/app/Models/ArtistContact.php @@ -54,7 +54,7 @@ final class ArtistContact extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function artist(): BelongsTo diff --git a/api/app/Models/ArtistEngagement.php b/api/app/Models/ArtistEngagement.php index dbb58428..7849de8e 100644 --- a/api/app/Models/ArtistEngagement.php +++ b/api/app/Models/ArtistEngagement.php @@ -88,7 +88,7 @@ final class ArtistEngagement extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function organisation(): BelongsTo diff --git a/api/app/Models/Genre.php b/api/app/Models/Genre.php index 070f8e09..076b27db 100644 --- a/api/app/Models/Genre.php +++ b/api/app/Models/Genre.php @@ -44,7 +44,7 @@ final class Genre extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function organisation(): BelongsTo diff --git a/api/app/Models/Performance.php b/api/app/Models/Performance.php index 639775e2..5b16927e 100644 --- a/api/app/Models/Performance.php +++ b/api/app/Models/Performance.php @@ -56,7 +56,7 @@ final class Performance extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function engagement(): BelongsTo diff --git a/api/app/Models/Stage.php b/api/app/Models/Stage.php index 7bd60a68..539421e8 100644 --- a/api/app/Models/Stage.php +++ b/api/app/Models/Stage.php @@ -51,7 +51,7 @@ final class Stage extends Model { return LogOptions::defaults() ->logFillable() - ->dontSubmitEmptyLogs(); + ->dontLogEmptyChanges(); } public function event(): BelongsTo diff --git a/api/database/factories/AdvanceSectionFactory.php b/api/database/factories/AdvanceSectionFactory.php new file mode 100644 index 00000000..6db86f31 --- /dev/null +++ b/api/database/factories/AdvanceSectionFactory.php @@ -0,0 +1,46 @@ + */ +final class AdvanceSectionFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'engagement_id' => ArtistEngagement::factory(), + 'name' => fake()->randomElement(['Gastenlijst', 'Contacts', 'Productie', 'Catering']), + 'type' => fake()->randomElement(AdvanceSectionType::cases()), + 'is_open' => false, + 'sort_order' => 0, + 'submission_status' => AdvanceSectionSubmissionStatus::Open, + ]; + } + + public function open(): static + { + return $this->state(fn () => [ + 'is_open' => true, + 'open_from' => now()->subDays(7), + 'open_to' => now()->addDays(14), + 'submission_status' => AdvanceSectionSubmissionStatus::Open, + ]); + } + + public function approved(): static + { + return $this->state(fn () => [ + 'submission_status' => AdvanceSectionSubmissionStatus::Approved, + 'last_submitted_at' => now()->subDay(), + ]); + } +} diff --git a/api/database/factories/AdvanceSubmissionFactory.php b/api/database/factories/AdvanceSubmissionFactory.php new file mode 100644 index 00000000..de72f0b9 --- /dev/null +++ b/api/database/factories/AdvanceSubmissionFactory.php @@ -0,0 +1,27 @@ + */ +final class AdvanceSubmissionFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'advance_section_id' => AdvanceSection::factory(), + 'submitted_by_name' => fake()->name(), + 'submitted_by_email' => fake()->safeEmail(), + 'submitted_at' => now()->subHours(fake()->numberBetween(1, 72)), + 'status' => AdvanceSubmissionStatus::Pending, + 'data' => ['payload' => fake()->sentence()], + ]; + } +} diff --git a/api/database/factories/ArtistContactFactory.php b/api/database/factories/ArtistContactFactory.php new file mode 100644 index 00000000..9334dda4 --- /dev/null +++ b/api/database/factories/ArtistContactFactory.php @@ -0,0 +1,42 @@ + */ +final class ArtistContactFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'artist_id' => Artist::factory(), + 'name' => fake()->name(), + 'email' => fake()->safeEmail(), + 'phone' => fake()->phoneNumber(), + 'role' => fake()->randomElement(['tour_manager', 'agent', 'manager', 'production_manager']), + 'is_primary' => false, + 'receives_briefing' => false, + 'receives_infosheet' => false, + ]; + } + + public function primary(): static + { + return $this->state(fn () => [ + 'is_primary' => true, + 'receives_briefing' => true, + 'receives_infosheet' => true, + ]); + } + + public function tourManager(): static + { + return $this->state(fn () => ['role' => 'tour_manager']); + } +} diff --git a/api/database/factories/ArtistEngagementFactory.php b/api/database/factories/ArtistEngagementFactory.php new file mode 100644 index 00000000..14150a1c --- /dev/null +++ b/api/database/factories/ArtistEngagementFactory.php @@ -0,0 +1,84 @@ + */ +final class ArtistEngagementFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + // organisation_id is set by the observer from the artist; + // factory leaves it null and lets the observer denormalise. + 'artist_id' => Artist::factory(), + 'event_id' => Event::factory(), + 'booking_status' => ArtistEngagementStatus::Draft, + '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' => 0, + 'guests_count' => 0, + 'advancing_completed_count' => 0, + 'advancing_total_count' => 0, + ]; + } + + public function draft(): static + { + return $this->state(fn () => ['booking_status' => ArtistEngagementStatus::Draft]); + } + + public function requested(): static + { + return $this->state(fn () => [ + 'booking_status' => ArtistEngagementStatus::Requested, + 'requested_at' => now(), + ]); + } + + public function option(): static + { + return $this->state(fn () => [ + 'booking_status' => ArtistEngagementStatus::Option, + 'option_expires_at' => now()->addDays(14), + ]); + } + + public function offered(): static + { + return $this->state(fn () => ['booking_status' => ArtistEngagementStatus::Offered]); + } + + public function confirmed(): static + { + return $this->state(fn () => ['booking_status' => ArtistEngagementStatus::Confirmed]); + } + + public function contracted(): static + { + return $this->state(fn () => [ + 'booking_status' => ArtistEngagementStatus::Contracted, + 'fee_amount' => fake()->randomFloat(2, 500, 25000), + 'portal_token' => (string) Str::ulid(), + ]); + } + + public function cancelled(): static + { + return $this->state(fn () => ['booking_status' => ArtistEngagementStatus::Cancelled]); + } +} diff --git a/api/database/factories/ArtistFactory.php b/api/database/factories/ArtistFactory.php new file mode 100644 index 00000000..6306f508 --- /dev/null +++ b/api/database/factories/ArtistFactory.php @@ -0,0 +1,57 @@ + */ +final class ArtistFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + $name = fake()->unique()->company().' '.fake()->randomElement(['Live', 'Sound', 'Project', 'Collective', 'DJ Set']); + + return [ + 'organisation_id' => Organisation::factory(), + 'name' => $name, + 'slug' => Str::slug($name).'-'.Str::lower(Str::random(4)), + 'default_genre_id' => null, + 'default_draw' => fake()->numberBetween(50, 5000), + 'star_rating' => fake()->numberBetween(1, 5), + 'home_base_country' => fake()->randomElement(['NL', 'BE', 'DE', 'FR', 'UK']), + 'agent_company_id' => null, + 'notes' => null, + ]; + } + + public function withGenre(?Genre $genre = null): static + { + return $this->state(function (array $attrs) use ($genre): array { + $resolved = $genre ?? Genre::factory()->create([ + 'organisation_id' => $attrs['organisation_id'], + ]); + + return ['default_genre_id' => $resolved->id]; + }); + } + + public function withAgent(?Company $company = null): static + { + return $this->state(function (array $attrs) use ($company): array { + $resolved = $company ?? Company::factory()->create([ + 'organisation_id' => $attrs['organisation_id'], + 'type' => 'agency', + ]); + + return ['agent_company_id' => $resolved->id]; + }); + } +} diff --git a/api/database/factories/GenreFactory.php b/api/database/factories/GenreFactory.php new file mode 100644 index 00000000..11ad5b40 --- /dev/null +++ b/api/database/factories/GenreFactory.php @@ -0,0 +1,25 @@ + */ +final class GenreFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'organisation_id' => Organisation::factory(), + 'name' => fake()->unique()->words(2, true), + 'color' => fake()->hexColor(), + 'sort_order' => 0, + 'is_active' => true, + ]; + } +} diff --git a/api/database/factories/PerformanceFactory.php b/api/database/factories/PerformanceFactory.php new file mode 100644 index 00000000..bdaffa5d --- /dev/null +++ b/api/database/factories/PerformanceFactory.php @@ -0,0 +1,48 @@ + */ +final class PerformanceFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + $start = CarbonImmutable::now()->addDays(7)->setTime(20, 0); + + return [ + 'engagement_id' => ArtistEngagement::factory(), + 'event_id' => Event::factory(), + 'stage_id' => Stage::factory(), + 'lane' => 0, + 'start_at' => $start, + 'end_at' => $start->addMinutes(60), + 'version' => 0, + 'notes' => null, + ]; + } + + public function parked(): static + { + return $this->state(fn () => ['stage_id' => null]); + } + + public function scheduled(Stage $stage, CarbonImmutable $start, int $minutes): static + { + return $this->state(fn () => [ + 'stage_id' => $stage->id, + 'event_id' => $stage->event_id, + 'start_at' => $start, + 'end_at' => $start->addMinutes($minutes), + ]); + } +} diff --git a/api/database/factories/StageFactory.php b/api/database/factories/StageFactory.php new file mode 100644 index 00000000..091b6e4e --- /dev/null +++ b/api/database/factories/StageFactory.php @@ -0,0 +1,30 @@ + */ +final class StageFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'event_id' => Event::factory(), + 'name' => fake()->unique()->randomElement(['Mainstage', 'Havana', 'Stairway', 'Socialite', 'Tent', 'Open Air', 'Greenhouse']), + 'color' => fake()->hexColor(), + 'capacity' => fake()->numberBetween(200, 5000), + 'sort_order' => 0, + ]; + } + + public function withCapacity(int $capacity): static + { + return $this->state(fn () => ['capacity' => $capacity]); + } +} diff --git a/api/database/seeders/ArtistTimetableDevSeeder.php b/api/database/seeders/ArtistTimetableDevSeeder.php new file mode 100644 index 00000000..0529a3a9 --- /dev/null +++ b/api/database/seeders/ArtistTimetableDevSeeder.php @@ -0,0 +1,209 @@ + $subEvents Sub-events keyed 0..n in date order */ + public static function seedForFestival(Organisation $org, Event $festival, array $subEvents): void + { + if (count($subEvents) < 3) { + return; + } + + // Genres + $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' => '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], + ])->mapWithKeys(fn (array $s) => [ + $s['name'] => Stage::create(['event_id' => $festival->id, ...$s]), + ]); + + // stage_days — every stage active every sub-event day + foreach ($stages as $stage) { + foreach ($subEvents as $subEvent) { + StageDay::create([ + 'stage_id' => $stage->id, + 'event_id' => $subEvent->id, + ]); + } + } + + // Master 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], + ['name' => 'Rotterdam Brass', 'genre' => 'Live band', 'draw' => 900, 'star' => 4], + ['name' => 'Nachtwacht DJs', 'genre' => 'Techno', 'draw' => 1100, 'star' => 3], + ['name' => 'De Lichtbrigade', 'genre' => 'Live band', 'draw' => 500, 'star' => 3], + ]; + + $artists = []; + foreach ($artistsData as $data) { + /** @var Artist $artist */ + $artist = Artist::create([ + 'organisation_id' => $org->id, + 'name' => $data['name'], + 'default_genre_id' => $genres[$data['genre']]->id, + 'default_draw' => $data['draw'], + 'star_rating' => $data['star'], + 'home_base_country' => 'NL', + ]); + $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 = []; + foreach ($statusPlan as $idx => $plan) { + /** @var Event $subEvent */ + $subEvent = $subEvents[$plan['sub']]; + $artist = $artists[$plan['artist']]; + + $attrs = [ + 'artist_id' => $artist->id, + 'event_id' => $subEvent->id, + 'booking_status' => $plan['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' => 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) { + $attrs['requested_at'] = CarbonImmutable::now()->subDays(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 + // 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 + [1, 'Mainstage', 1, 23, 30, 60], // Donker & Licht — Zaterdag mainstage (B2B partner-A) + [1, 'Mainstage', 1, 21, 0, 60], // …extra perf same engagement (D17 multi-perf) + [2, 'Havana', 0, 21, 0, 60], + [3, 'Havana', 1, 22, 30, 60], + [4, 'Stairway', 1, 21, 0, 75], + [5, 'Socialite', 0, 19, 0, 60], + [6, 'Socialite', 2, 17, 0, 45], + [7, 'Mainstage', 1, 23, 33, 60], // B2B partner-B (3-min offset → seeds B2B detector) + [9, null, 0, 0, 0, 60], // De Lichtbrigade Vrijdag = parked / wachtrij + [10, 'Stairway', 1, 19, 0, 60], + [11, 'Havana', 2, 20, 30, 60], + [4, 'Stairway', 1, 22, 30, 60], // multi-perf on same engagement (D17) + ]; + + foreach ($perfPlan as $row) { + [$engagementIdx, $stageName, $subIdx, $hour, $minute, $minutes] = $row; + $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, + ]); + } + } +} diff --git a/api/database/seeders/DevSeeder.php b/api/database/seeders/DevSeeder.php index 7f32f070..f3e4f331 100644 --- a/api/database/seeders/DevSeeder.php +++ b/api/database/seeders/DevSeeder.php @@ -756,7 +756,7 @@ class DevSeeder extends Seeder foreach ($approvedPersons->shuffle() as $person) { $existing = $usedPersonSlots[$person->id] ?? []; - $available = $openShifts->filter(fn (Shift $shift) => !in_array($shift->time_slot_id, $existing)); + $available = $openShifts->filter(fn (Shift $shift) => ! in_array($shift->time_slot_id, $existing)); if ($available->isEmpty()) { continue; @@ -956,6 +956,15 @@ class DevSeeder extends Seeder FormBuilderDevSeeder::seedEventRegistrationShowcase($this->org, $festival, $this->command); } + // RFC-TIMETABLE v0.2 — artist + timetable fixture for the + // festival (4 stages, 6 artists, 12 engagements, 13 perfs). + ArtistTimetableDevSeeder::seedForFestival( + $this->org, + $festival, + [$vrijdag, $zaterdag, $zondag], + ); + $this->command->info(' Artist timetable: 4 stages, 12 stage_days, 6 artists, 12 engagements, 13 performances'); + $this->command->info(' Echt Feesten 2026 complete'); }); } @@ -1063,7 +1072,7 @@ class DevSeeder extends Seeder 'organisation_id' => $this->org->id, 'parent_event_id' => $ijsbaan->id, 'name' => $wd['name'], - 'slug' => 'ijsbaan-week-' . ($i + 1), + 'slug' => 'ijsbaan-week-'.($i + 1), 'start_date' => $wd['start'], 'end_date' => $wd['end'], 'timezone' => 'Europe/Amsterdam', @@ -1192,7 +1201,7 @@ class DevSeeder extends Seeder $this->createCrowdList($ijsbaan, 'IJsbaan Vaste Crew', CrowdListType::INTERNAL, $this->crowdTypes['CREW'], null, false, null, $approvedCrew, $bert); $personCount = Person::where('event_id', $ijsbaan->id)->count(); - $this->command->info(" {$personCount} persons, " . count($allShifts) . ' shifts created'); + $this->command->info(" {$personCount} persons, ".count($allShifts).' shifts created'); $formSchema = FormBuilderDevSeeder::seedEventSchema($ijsbaan); $submissions = FormBuilderDevSeeder::seedSubmissionsForEvent($ijsbaan, $formSchema); -- 2.39.5 From dd0d98f9ed4f9702e886312c73e386bb549e123a Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:08:52 +0200 Subject: [PATCH 06/14] =?UTF-8?q?refactor(timetable):=20PURPOSE=5FSUBJECT?= =?UTF-8?q?=5FFQCN=20=E2=80=94=20Artist::class=20instead=20of=20string-lit?= =?UTF-8?q?eral?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- api/app/Providers/AppServiceProvider.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php index 0aaf16ad..98a1e653 100644 --- a/api/app/Providers/AppServiceProvider.php +++ b/api/app/Providers/AppServiceProvider.php @@ -16,6 +16,7 @@ use App\Listeners\FormBuilder\ApplyBindingsOnFormSectionSubmitted; use App\Listeners\FormBuilder\ApplyBindingsOnFormSubmit; use App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit; use App\Listeners\FormBuilder\TriggerPersonIdentityMatchOnFormSubmit; +use App\Models\Artist; use App\Models\Company; use App\Models\CrowdList; use App\Models\CrowdType; @@ -72,17 +73,16 @@ class AppServiceProvider extends ServiceProvider /** * FQCN lookup for every `subject_type` alias that PurposeRegistry may * return. Single source of truth for domain-subject → model mapping, - * consumed by the morph-map build in boot(). The `artist` class is - * declared as a string because the Artist model is not yet landed; - * it is safe to register the morph alias (lazily resolved). + * consumed by the morph-map build in boot(). `artist` resolves to + * `App\Models\Artist` (introduced 2026-05, RFC-TIMETABLE Session 1). * - * @var array + * @var array */ private const PURPOSE_SUBJECT_FQCN = [ 'person' => Person::class, 'user' => User::class, 'company' => Company::class, - 'artist' => 'App\\Models\\Artist', + 'artist' => Artist::class, ]; public function register(): void -- 2.39.5 From 7e4db29b2b1037234cfdb0cdf4f6dac10467d752 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:47:27 +0200 Subject: [PATCH 07/14] =?UTF-8?q?docs(schema):=20rewrite=20=C2=A73.5.7=20A?= =?UTF-8?q?rtists=20&=20Advancing=20=E2=80=94=20RFC=20v0.2=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dev-docs/SCHEMA.md | 343 +++++++++++++++++++++++++++++---------------- 1 file changed, 223 insertions(+), 120 deletions(-) diff --git a/dev-docs/SCHEMA.md b/dev-docs/SCHEMA.md index fe102b95..e727e113 100644 --- a/dev-docs/SCHEMA.md +++ b/dev-docs/SCHEMA.md @@ -1150,162 +1150,265 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date; ## 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 | -| ------------------------------ | ------------------ | -------------------------------------------------------------- | -| `id` | ULID | PK | -| `event_id` | ULID FK | → events | -| `name` | string | | -| `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 | +The artist domain splits into a **master record** (`artists`, +org-scoped) and a **per-event booking** (`artist_engagements`). +The split allows one artist to have multiple engagements across +events, each with its own deal, advance trajectory, and timetable +performances. -**Relations:** `hasMany` performances, advance_sections, artist_contacts, artist_riders -**Soft delete:** yes +### `genres` + +| 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 | -| ----------------- | ------- | ------------------------------- | -| `id` | ULID | PK | -| `artist_id` | ULID FK | → artists | -| `stage_id` | ULID FK | → stages | -| `date` | date | | -| `start_time` | time | | -| `end_time` | time | | -| `booking_status` | string | | -| `check_in_status` | enum | `expected\|checked_in\|no_show` | +| Column | Type | Notes | +| ------------------- | ---------------- | ------------------------------------------- | +| `id` | ULID | PK | +| `organisation_id` | ULID FK | → organisations | +| `name` | string(120) | | +| `slug` | string(120) | unique per organisation, generated from name | +| `default_genre_id` | ULID FK nullable | → genres (D24) | +| `default_draw` | int nullable | for capacity-warn defaults | +| `star_rating` | tinyint nullable | 1–5 | +| `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` -| Column | Type | Notes | -| ---------- | ------------ | -------- | -| `id` | ULID | PK | -| `event_id` | ULID FK | → events | -| `name` | string | | -| `color` | string | hex | -| `capacity` | int nullable | | +| Column | Type | Notes | +| ------------ | ------------ | -------------------------------- | +| `id` | ULID | PK | +| `event_id` | ULID FK | → events (festival or flat event) | +| `name` | string(120) | | +| `color` | string(7) | hex | +| `capacity` | int nullable | drives capacity-warn (RFC D23) | +| `sort_order` | int default 0 | (RFC D23) | +| `created_at`, `updated_at` | timestamps | | -**Relations:** `hasMany` performances -**Indexes:** `(event_id)` +**Unique:** `UNIQUE(event_id, name)` +**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` -| Column | Type | Notes | -| ---------- | ------- | --------------------------------- | -| `id` | int AI | PK — integer for join performance | -| `stage_id` | ULID FK | → stages | -| `day_date` | date | | +| Column | Type | Notes | +| ----------- | ------- | ---------------------------------------------- | +| `id` | int AI | PK — integer for join performance | +| `stage_id` | ULID FK | → stages | +| `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 | -| ------------------- | ------------------ | -------------------------------------------------------------- | -| `id` | ULID | PK | -| `artist_id` | ULID FK | → artists | -| `name` | string | | -| `type` | enum | `guest_list\|contacts\|production\|custom` | -| `is_open` | bool | | -| `open_from` | datetime nullable | | -| `open_to` | datetime nullable | | -| `sort_order` | int | | -| `submission_status` | enum | `open\|pending\|submitted\|approved\|declined` | -| `last_submitted_at` | timestamp nullable | | -| `last_submitted_by` | string nullable | | -| `submission_diff` | JSON nullable | `{created, updated, untouched, deleted}` counts per submission | +| Column | Type | Notes | +| ---------------------------- | -------------------------- | ---------------------------------------------------- | +| `id` | ULID | PK | +| `organisation_id` | ULID FK | → organisations (denormalised — observer-maintained) | +| `artist_id` | ULID FK | → artists (master) | +| `event_id` | ULID FK | → events (festival or flat event) | +| `booking_status` | string | RFC D9 — see `App\Enums\Artist\ArtistEngagementStatus` | +| `project_leader_id` | ULID FK nullable | → users | +| `fee_amount` | decimal(10,2) nullable | | +| `fee_currency` | string(3) default 'EUR' | | +| `fee_type` | string nullable | `App\Enums\Artist\FeeType` | +| `buma_applicable` | bool default true | | +| `buma_percentage` | decimal(5,2) default 7.00 | | +| `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` -| Column | Type | Notes | -| -------------------- | ------------------ | ------------------------------ | -| `id` | ULID | PK | -| `advance_section_id` | ULID FK | → advance_sections | -| `submitted_by_name` | string | | -| `submitted_by_email` | string | | -| `submitted_at` | timestamp | | -| `status` | enum | `pending\|accepted\|declined` | -| `reviewed_by` | ULID FK nullable | → users | -| `reviewed_at` | timestamp nullable | | -| `data` | JSON | Free form data — not queryable | +| Column | Type | Notes | +| -------------------- | --------------------- | ------------------------------ | +| `id` | ULID | PK | +| `advance_section_id` | ULID FK | → advance_sections | +| `submitted_by_name` | string | | +| `submitted_by_email` | string | | +| `submitted_at` | timestamp | | +| `status` | string default 'pending' | `App\Enums\Artist\AdvanceSubmissionStatus` | +| `reviewed_by` | ULID FK nullable | → users | +| `reviewed_at` | timestamp nullable | | +| `data` | JSON | free-form payload — not queryable | +| `created_at`, `updated_at` | timestamps | | **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 | -| -------------------- | --------------- | -------------------------------- | -| `id` | ULID | PK | -| `artist_id` | ULID FK | → artists | -| `name` | string | | -| `email` | string nullable | | -| `phone` | string nullable | | -| `role` | string | e.g. tour manager, agent, booker | -| `receives_briefing` | bool | | -| `receives_infosheet` | bool | | -| `is_travel_party` | bool | | +| Enum | Values | Notes | +| --------------------------------- | ----------------------------------------------------------------------------------------- | ----- | +| `ArtistEngagementStatus` | `draft`, `requested`, `option`, `offered`, `confirmed`, `contracted`, `cancelled`, `rejected`, `declined` | RFC D9 — Dutch labels via `label()` | +| `BumaHandledBy` | `organisation`, `booking_agency`, `not_applicable` | RFC D26 | +| `FeeType` | `flat`, `door_split`, `guarantee_plus_split` | | +| `PaymentStatus` | `none`, `deposit_paid`, `paid_in_full` | | +| `AdvanceSectionType` | `guest_list`, `contacts`, `production`, `custom` | | +| `AdvanceSectionSubmissionStatus` | `open`, `pending`, `submitted`, `approved`, `declined` | | +| `AdvanceSubmissionStatus` | `pending`, `accepted`, `declined` | | -**Indexes:** `(artist_id)` - ---- - -### `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)` +> **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. --- -- 2.39.5 From ad6bf3b44d0bc0b903c928bfe158528a29695f73 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:48:38 +0200 Subject: [PATCH 08/14] docs(form-builder): align artist_advance with engagement-scoped sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §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) --- dev-docs/ARCH-FORM-BUILDER.md | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/dev-docs/ARCH-FORM-BUILDER.md b/dev-docs/ARCH-FORM-BUILDER.md index f6d72131..6d190f11 100644 --- a/dev-docs/ARCH-FORM-BUILDER.md +++ b/dev-docs/ARCH-FORM-BUILDER.md @@ -347,18 +347,31 @@ company-type (supplier/partner/etc.), KvK-nummer, BTW-nummer. **Who creates:** org-level schema (one per org), seeded from `ArtistAdvanceDefault`. Schema has `section_level_submit=true`. -**Who submits:** artist via portal_token (no authentication required). +**Who submits:** tour manager / artist representative via the +engagement's `portal_token` (RFC-TIMETABLE v0.2 §5.3) — no +authentication required. + +**Section model:** `advance_sections` rows are scoped to +`engagement_id` (per-event), not to `artist_id` (master). One master +artist with two engagements (e.g. Vrijdag and Zaterdag of the same +festival) advances each engagement on its own trajectory. +Section-type categorisation is driven by the `AdvanceSectionType` +enum (see RFC v0.2 §10 + `App\Enums\Artist\AdvanceSectionType` — +`guest_list | contacts | production | custom`). Section labels +live in `advance_sections.name` (string 80). Default-section seeds +are defined in Session 3 (`ArtistAdvanceDefault`). **Submission flow:** -1. Artist receives email with portal link (triggered by organiser) -2. Opens link → token resolved, submission fetched or created -3. Form shown with sections: General Info, Contacts, Production (power, - catering, transport), Technical Rider, Hospitality -4. Artist fills sections independently; each section submit separately -5. On section submit: FormSubmissionSectionSubmitted event fires +1. Tour manager receives email with portal link (triggered by organiser) +2. Opens link → engagement resolved from `portal_token`, submission + fetched or created with the master `Artist` as subject (see §17.3) +3. Form shown with the engagement's `advance_sections` +4. Sections are filled independently; each section submits separately +5. On section submit: `FormSubmissionSectionSubmitted` event fires 6. Organiser reviews per section → section status approved/rejected/ changes_requested -7. When all sections approved: FormSubmissionSubmitted fires at submission level +7. When all sections approved: `FormSubmissionSubmitted` fires at the + submission level 8. Integration: triggers accreditation generation per ARCH-07 (future) **Mode:** draft_single. @@ -2358,8 +2371,10 @@ support. | Slug | Label (NL) | Subject type | Default submission mode | Public access | Required bindings | |------|-----------|--------------|-------------------------|---------------|-------------------| | `event_registration` | Aanmelding vrijwilligers/crew | person | single | yes | `person.email`, `person.first_name`, `person.last_name` | -| `artist_advance` | Artiest advance | artist | draft_single | no | — | +| `artist_advance` | Artiest advance | artist | draft_single | no | — |[^artist-advance-engagement] | | `supplier_intake` | Leverancier intake | company | single | no | `company.name` | + +[^artist-advance-engagement]: *Engagement context resolution: `ArtistResolver::fromPortalToken` looks up `artist_engagements.portal_token` to find the engagement, then returns the engagement's master `Artist` as `subject`. The engagement's `event_id` populates `form_submissions.event_id` per WS-4 denormalisation. The advance-section breakdown lives per engagement (see §3.2.5 + RFC-TIMETABLE v0.2 §5.3), so multi-event artists get multi-trajectory advancing.* | `post_event_evaluation` | Evaluatie na afloop | person | single | no | — | | `incident_report` | Incident-melding | person | multiple | no | — | | `signature_contract` | Contract-ondertekening | user | single | no | — | -- 2.39.5 From 4e5671daa99ed7e3fc39f45730a33e13e6951f22 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 18:50:17 +0200 Subject: [PATCH 09/14] docs(backlog): close ARCH-09; open ART-OBSERVER-ADVANCE-AGGREGATE + RFC-TIMETABLE-V0.2-DOC-CLEANUP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dev-docs/BACKLOG.md | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 2b6c04e8..88caa278 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -676,6 +676,34 @@ voor third-party integraties (ticketing, HR, etc.) ## Technische schuld +### ART-OBSERVER-ADVANCE-AGGREGATE — Recompute `advancing_*_count` op AdvanceSection lifecycle + +**Aanleiding:** RFC-TIMETABLE v0.2 Session 1 landed `advancing_completed_count` en +`advancing_total_count` op `artist_engagements` met `default 0`. De observer die +deze tellers bijwerkt op AdvanceSection-lifecycle wijzigingen is nog niet geland. +**Wat:** Implementeer een `AdvanceSectionObserver` die op create/update/delete +(en submission-status transitions) de aggregaat-tellers op de parent-engagement +herberekent. Trigger: section-level submit lands in Session 3. +**Prioriteit:** Middel — alleen relevant zodra de section-lifecycle daadwerkelijk +bestaat. Voor Session 1 is `default 0` voldoende. + +--- + +### RFC-TIMETABLE-V0.2-DOC-CLEANUP — Strip ARCH-PLANNED-MODULES.md mentions from RFC v0.2 + +**Aanleiding:** RFC-TIMETABLE v0.2 §1 ("Bron-documenten") en §15 +("Implementatieplan") verwijzen naar `ARCH-PLANNED-MODULES.md §3.5.7` als de +locatie waar de oude artist-schema-planning leefde. Dat bestand bestaat niet +in de repo — de oude §3.5.7 leefde rechtstreeks in `SCHEMA.md`. Phase A van +Session 1 surfaced deze drift; Step 7 reduceerde tot een in-place rewrite van +`SCHEMA.md` §3.5.7. De RFC zelf is Approved en frozen — niet ad-hoc patchen. +**Wat:** Bij de eerstvolgende RFC v0.2 amendement, vervang of verwijder de +`ARCH-PLANNED-MODULES.md` cross-references in §1 en §15. Dit is geen blocker +voor implementatie van Sessions 2–6. +**Prioriteit:** Laag — documentatie-hygiëne, niet code. + +--- + ### TECH-01 — Bestaande tests bijwerken na festival/event refactor **Aanleiding:** Na toevoegen parent_event_id worden bestaande tests @@ -1178,6 +1206,7 @@ deadline implementation). - ~~**TECH-DOCS-APPS-PORTAL-PURGE**: per-file DELETE/REWRITE/KEEP_AND_PURGE matrix uitgevoerd op alle 9 docs uit de oorspronkelijke entry, plus de `post-edit-eslint.sh` hook (out-of-scope vondst uit Phase A). Vijf obsolete docs verwijderd (`.cursor/instructions.md`, `.cursor/ARCHITECTURE.md`, `dev-docs/MASTER_PROMPT_CC.md`, `dev-docs/MASTER_PROMPT_CURSOR.md`, `dev-docs/dev-guide.md` — totaal ~80 KB). Drie herschreven (`SETUP.md`, `101_vue.mdc`, hook-script). Twee chirurgisch gepurgeerd (`102_multi_tenancy.mdc`, `CLAUDE_CODE_TOOLING.md`). Externe verwijzingen in README.md, CLAUDE_DESKTOP_SETUP.md, ARCH-CONSOLIDATION-2026-04.md en VIBE_CODING_CHECKLIST.md mee bijgewerkt. WS-3 PR-C op 2026-05-06. Single SPA, single cookie, single deploy host. WS-3 compleet.~~ ✅ - ~~**WS-7 Observability — closure (mei 2026)**: 4 PRs gemerged op `feat/ws-7-observability` (infra `5f6fc07`, backend SDK `bdb89a2..0379016`, frontend SDK `bc47783..5c42f27`, docs `754222f..e9da01f`). 1551 backend + 252 frontend tests groen. Acceptance criteria 1-14 voldaan; observability volledig operationeel op `monitoring.hausdesign.nl`. Implementation criteria 3, 4, 5, 6, 8, 11, 12, 13, 14 via PRs; operationele criteria 1, 2, 7, 9, 10 via deploy-checklist (DNS, TLS, superuser+2FA, prod DSNs, email-alerting, retention 90d, cron backup). Architecturale patronen vastgelegd in `dev-docs/ARCH-OBSERVABILITY.md` (730 regels) + 2 runbooks (`observability-triage.md`, `observability-erasure.md`). Twee GlitchTip projecten (`crewli-api` + `crewli-app`), één DSN per project, runtime context-split via `actor_scope` tag. Patronen: explicit > implicit listener registration, default-in-listener / override-in-middleware voor binary tags, tenant resolution chain (route-param → portal-token → super_admin platform → user fallback). Volgsporen: OBS-1, OBS-4, OBS-6, OBS-7, OBS-9 (zie "Observability follow-ups" sectie hieronder).~~ ✅ - ~~**WS-6 v1.3-delta — closure (mei 2026)**: Architecturele review-sessie 2026-05-07 identificeerde vijf verfijningen op RFC-WS-6 v1.2 (Q1 listener queueing, Q2 invariant cleanup, Q3 failure-UX additions, plus §19 BACKLOG-pointer). v1.3 amendement gecommit (`845b6e6`, 2026-05-07); v1.3.1 drift closure (`b255879`, 2026-05-08) sloot code-vs-docs gaten pre-implementation. Implementatie geland als D1 + D2: **D1** (PR #10 `c6f4d1b`) leverde de data-laag — `failure_response_code` kolom op `form_submissions`, abstract `FormBindingApplicatorException` hiërarchie + 4 reason-coded subclasses (`FormBindingSchemaConfigException`, `FormBindingInfraException`, `FormBindingApplicatorTimeoutException`, `FormBindingDataIntegrityException`), `IdentityMatchInvariantViolation` sibling, `FormBindingExceptionClassifier` helper, `FormSubmissionIdentityMatchResolved` broadcast event class, `FormFieldBindingMergeStrategy::validForTargetType` matrix method, cast + factory state. **D2** (PR #11 `23a5696`) wired alle building blocks in de listener-chain — `ApplyBindings` initial `pending` write + deadline wrapper + classifier in catch; `TriggerPersonIdentityMatch` queued + gating-invariant + invariant throw + broadcast dispatch; `routes/channels.php` + bootstrap routing (NIEUWE broadcast wiring, submitter-only auth); gating-invariant op `SyncTagPicker`; `AppServiceProvider::boot` v1.3 layout; `FormFailureRetryService::recordFailure` classifier + apply_completed_at symmetrie-fix; `apply_deadline_seconds` config key (default 5). Tests: pre-WS-6 baseline 1208 → pre-D1 1551 → post-D2 1621. 0 Larastan errors. Phase F (`ConditionalRequirement(public_token)` wrapper drop) was no-op — change had silently landed pre-D2. **Open follow-ups:** `TECH-CHANNEL-AUTH-ORG-ADMIN` (extend `submission.{id}` channel auth to org admins na Spatie Permission helper-audit); GlitchTip alert rule op `apply_status=failed AND form_schema.has_public_token=true` (operationele taak in GlitchTip web-UI op `monitoring.hausdesign.nl`; runbook procedure in `dev-docs/runbooks/observability-triage.md` §7); frontend Echo subscription voor `FormSubmissionIdentityMatchResolved` (separate frontend follow-up, out of WS-6 scope, backend-infra ready). `PARTIAL-BINDING-SUCCESS` en `FORM-SCHEMA-DRIFT-DETECTION` blijven open conform v1.3 amendement (trigger-condities nog niet gevuurd). Closure docs-PR: RFC-WS-6.md v1.3.1 implementation-status marker + §10 closure entry, ARCH-BINDINGS.md v1.2 onveranderd, runbook §7 toegevoegd.~~ ✅ +- ~~**ARCH-09 — Artist Eloquent model + migration — closure (mei 2026)**: Foundation for the Artist & Timetable module landed as RFC-TIMETABLE v0.2 Session 1 op `feat/timetable-session-1`. Delivered: 10 migrations (genres, artists, companies.handles_buma column, artist_contacts, stages, stage_days, artist_engagements, performances, advance_sections, advance_submissions); 7 PHP enums under `App\Enums\Artist\` (`ArtistEngagementStatus` D9 with Dutch labels, `BumaHandledBy` D26, `FeeType`, `PaymentStatus`, `AdvanceSectionType`, `AdvanceSectionSubmissionStatus`, `AdvanceSubmissionStatus`); 9 Eloquent models with `OrganisationScope` (direct on Artist/Genre/ArtistEngagement, FK-chain via `tenantScopeStrategy()` on the rest) and `LogsActivity` baseline; 2 observers (`ArtistEngagementObserver` for `organisation_id` denorm + cross-tenant guard via `CrossTenantEngagementException` + cascade soft-delete to performances + hard-delete to advance_sections; `PerformanceObserver` for D14 optimistic-lock `version` bump on UPDATE); 8 factories + `ArtistTimetableDevSeeder` reproducing the prototype fixture (4 stages, 12 stage_days, 6 artists, 12 engagements, 13 performances incl. 1 parked); `PURPOSE_SUBJECT_FQCN` switched from string-literal to `Artist::class` (MorphMapAlignmentTest green); SCHEMA.md §3.5.7 rewritten in place (ARCH-PLANNED-MODULES.md was assumed by the RFC pre-amble but did not exist — see `RFC-TIMETABLE-V0.2-DOC-CLEANUP`); ARCH-FORM-BUILDER.md §3.2.5 updated for engagement-scoped sections and §17.3 footnote on `ArtistResolver::fromPortalToken` engagement context resolution. PR #XX, 2026-05-08.~~ ✅ - ~~**TECH-CHANNEL-AUTH-ORG-ADMIN — closure (mei 2026)**: `submission.{id}` private channel auth uitgebreid van submitter-only naar drie-paths: submitter (`submitted_by_user_id === user.id`) → super_admin Spatie HasRoles app-wide bypass → org_admin van submission's organisatie via pivot-table check op `user_organisation` (`->wherePivot('role', 'org_admin')`). Pattern: directe port van `FormSubmissionActionFailurePolicy::canAccess`, codebase canonical (gebruikt in 17+ policy sites). Spatie teams is disabled in `config/permission.php`, dus org-scoping leeft in de pivot, niet in Spatie. **super_admin bypass is een audit-surfaced bonus** (origineel BACKLOG entry vroeg alleen om org-admin extension; tijdens Phase A audit bleek dat elke analoge policy super_admin bypass heeft, dus toegevoegd voor consistency — zonder die bypass zouden super_admins op de admin-panel banner mysterieus geen live updates krijgen). Tests: 4 nieuw (`test_super_admin_can_subscribe`, `test_organisation_admin_of_submission_org_can_subscribe`, `test_organisation_admin_of_different_org_cannot_subscribe` (kritische cross-tenant guard), `test_regular_organisation_member_cannot_subscribe`); 1 verwijderd (de "should flip" denied-by-default test uit PR #11). Test count: 1621 → 1624 (+3 net). 0 Larastan errors. Inline TODO uit `routes/channels.php` verwijderd. Sibling `FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION` blijft open (frontend portal IdentityMatchBanner subscription is de pair met deze backend-auth uitbreiding).~~ ✅ --- @@ -1238,7 +1267,6 @@ Overzicht van bekende ontbrekende onderdelen die nog niet gebouwd zijn: | UX-01 — Festival setup checklist | Niet gestart | Middel | | UX-03 — Personen per sub-event | Niet gestart | Middel | | ARCH-06 — Locatie-gebaseerd shift-overzicht | Niet gestart | Laag | -| ARCH-09 — Artist Eloquent model + migration | Prerequisite for artist_advance purpose | Hoog (blocker voor artist_advance) | --- @@ -1267,18 +1295,6 @@ Herhalingsfunctie: "genereer 5 time slots in één keer" voor opbouwdagen etc. --- -### ARCH-09 — Artist Eloquent model + migration - -**Aanleiding:** `artist_advance` purpose is geregistreerd in `PurposeRegistry` (v1.0) met `subject_type = 'artist'`, maar het `App\Models\Artist` model en de `artists` tabel bestaan nog niet. `AppServiceProvider::PURPOSE_SUBJECT_FQCN` bevat `'artist' => 'App\\Models\\Artist'` als string-literal (gedocumenteerd in de constant-docblock) om morph-map-registratie te laten slagen — resolution is lazy en knalt pas bij de eerste echte artist-submission. - -**Wat:** Artist Eloquent model + migratie + factory, conform het patroon van de overige business-tabellen (ULID PK, `HasUlids`, `OrganisationScope`, soft deletes per SCHEMA §3.5.7). Na het landen van het model: `PURPOSE_SUBJECT_FQCN` omzetten van string-literal naar `Artist::class` import. - -**Prioriteit:** Hoog — blokkeert elke feature-sprint rond artist_advance. - -**Afhankelijk van:** SCHEMA §3.5.7 finalisatie (artists, performances, stages etc. — momenteel in `/dev-docs/ARCH-PLANNED-MODULES.md` na WS-8). - ---- - ### ART-03 — Artist profile met cross-event rider defaults Organisatie-niveau artiest-profiel dat rider-defaults, contacten en interne -- 2.39.5 From eb6d3966726167107dd439d74ef08fda7a1a61d4 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 19:15:02 +0200 Subject: [PATCH 10/14] fix(timetable): widen artist_engagements.portal_token to varchar(64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...100006_create_artist_engagements_table.php | 7 ++++-- api/database/schema/mysql-schema.sql | 22 +++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php b/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php index 7d8c9d58..977f9448 100644 --- a/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php +++ b/api/database/migrations/2026_05_08_100006_create_artist_engagements_table.php @@ -44,8 +44,11 @@ return new class extends Migration $table->datetime('advance_open_from')->nullable(); $table->datetime('advance_open_to')->nullable(); - // Portal access - $table->ulid('portal_token')->nullable()->unique(); + // Portal access. Stored as hashed token (sha256 hex, 64 chars). + // RFC v0.2 §5.3 says "ULID unique nullable"; in practice the + // PortalTokenController hashes the plain token and stores the + // 64-char hex digest. Widen the column to fit. + $table->string('portal_token', 64)->nullable()->unique(); // Advancing aggregates (recomputed in Session 3) $table->integer('advancing_completed_count')->default(0); diff --git a/api/database/schema/mysql-schema.sql b/api/database/schema/mysql-schema.sql index 41f58fae..fcc343e0 100644 --- a/api/database/schema/mysql-schema.sql +++ b/api/database/schema/mysql-schema.sql @@ -121,7 +121,7 @@ CREATE TABLE `artist_engagements` ( `option_expires_at` datetime DEFAULT NULL, `advance_open_from` datetime DEFAULT NULL, `advance_open_to` datetime DEFAULT NULL, - `portal_token` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `portal_token` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `advancing_completed_count` int NOT NULL DEFAULT '0', `advancing_total_count` int NOT NULL DEFAULT '0', `notes` text COLLATE utf8mb4_unicode_ci, @@ -1934,13 +1934,13 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (122,'2026_04_28_14 INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (123,'2026_04_28_180000_create_form_submission_action_failure_retry_attempts_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (124,'2026_04_28_181000_add_exception_trace_to_form_submission_action_failures',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (125,'2026_05_08_000001_add_failure_response_code_to_form_submissions',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (136,'2026_05_08_100000_create_genres_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (137,'2026_05_08_100001_create_artists_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (138,'2026_05_08_100002_add_handles_buma_to_companies_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (139,'2026_05_08_100003_create_artist_contacts_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (140,'2026_05_08_100004_create_stages_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (141,'2026_05_08_100005_create_stage_days_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (142,'2026_05_08_100006_create_artist_engagements_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (143,'2026_05_08_100007_create_performances_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (144,'2026_05_08_100008_create_advance_sections_table',2); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (145,'2026_05_08_100009_create_advance_submissions_table',2); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (126,'2026_05_08_100000_create_genres_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (127,'2026_05_08_100001_create_artists_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (128,'2026_05_08_100002_add_handles_buma_to_companies_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (129,'2026_05_08_100003_create_artist_contacts_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (130,'2026_05_08_100004_create_stages_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (131,'2026_05_08_100005_create_stage_days_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (132,'2026_05_08_100006_create_artist_engagements_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (133,'2026_05_08_100007_create_performances_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (134,'2026_05_08_100008_create_advance_sections_table',1); +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (135,'2026_05_08_100009_create_advance_submissions_table',1); -- 2.39.5 From 64878f2734a5c1e10a7b23181a752a1beb1ef9fa Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 19:15:13 +0200 Subject: [PATCH 11/14] fix(timetable): wire portal-token auth through artist_engagements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Api/V1/PortalTokenController.php | 24 ++-- .../Http/Middleware/PortalTokenMiddleware.php | 11 +- .../Security/PortalTokenSecurityTest.php | 107 ++++++++---------- 3 files changed, 73 insertions(+), 69 deletions(-) diff --git a/api/app/Http/Controllers/Api/V1/PortalTokenController.php b/api/app/Http/Controllers/Api/V1/PortalTokenController.php index 865b9bc1..1575d6a7 100644 --- a/api/app/Http/Controllers/Api/V1/PortalTokenController.php +++ b/api/app/Http/Controllers/Api/V1/PortalTokenController.php @@ -18,18 +18,28 @@ final class PortalTokenController extends Controller { $hashedToken = hash('sha256', $request->validated('token')); - // Try artists table - $artist = DB::table('artists')->where('portal_token', $hashedToken)->first(); + // Artist portal token lives on artist_engagements (per RFC-TIMETABLE + // v0.2 §5.3); join to artists for the master name. + $row = DB::table('artist_engagements') + ->join('artists', 'artists.id', '=', 'artist_engagements.artist_id') + ->where('artist_engagements.portal_token', $hashedToken) + ->select( + 'artist_engagements.id as id', + 'artist_engagements.event_id as event_id', + 'artist_engagements.booking_status as booking_status', + 'artists.name as name', + ) + ->first(); - if ($artist) { - $event = Event::withoutGlobalScope(OrganisationScope::class)->find($artist->event_id); + if ($row) { + $event = Event::withoutGlobalScope(OrganisationScope::class)->find($row->event_id); return response()->json([ 'context' => 'artist', 'data' => [ - 'id' => $artist->id, - 'name' => $artist->name, - 'booking_status' => $artist->booking_status, + 'id' => $row->id, + 'name' => $row->name, + 'booking_status' => $row->booking_status, ], 'event' => $event ? new PortalEventResource($event) : null, ]); diff --git a/api/app/Http/Middleware/PortalTokenMiddleware.php b/api/app/Http/Middleware/PortalTokenMiddleware.php index 2d36f3dd..206657e9 100644 --- a/api/app/Http/Middleware/PortalTokenMiddleware.php +++ b/api/app/Http/Middleware/PortalTokenMiddleware.php @@ -23,18 +23,19 @@ final class PortalTokenMiddleware $hashedToken = hash('sha256', $plainToken); - // Try artists table - $artist = DB::table('artists')->where('portal_token', $hashedToken)->first(); + // Artist portal token lives on artist_engagements (per RFC-TIMETABLE + // v0.2 §5.3); resolve to the engagement's event. + $engagement = DB::table('artist_engagements')->where('portal_token', $hashedToken)->first(); - if ($artist) { - $event = Event::withoutGlobalScope(OrganisationScope::class)->find($artist->event_id); + if ($engagement) { + $event = Event::withoutGlobalScope(OrganisationScope::class)->find($engagement->event_id); if (! $event || in_array($event->status, ['draft', 'closed'], true)) { return response()->json(['message' => 'Portal token required.'], 401); } $request->attributes->set('portal_context', 'artist'); - $request->attributes->set('portal_person', $artist); + $request->attributes->set('portal_person', $engagement); $request->attributes->set('portal_event', $event); return $next($request); diff --git a/api/tests/Feature/Security/PortalTokenSecurityTest.php b/api/tests/Feature/Security/PortalTokenSecurityTest.php index 0aa4dac4..249fdc60 100644 --- a/api/tests/Feature/Security/PortalTokenSecurityTest.php +++ b/api/tests/Feature/Security/PortalTokenSecurityTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Feature\Security; +use App\Models\Artist; use App\Models\Event; use App\Models\Organisation; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -16,6 +17,7 @@ final class PortalTokenSecurityTest extends TestCase use RefreshDatabase; private Organisation $organisation; + private Event $event; protected function setUp(): void @@ -59,30 +61,50 @@ final class PortalTokenSecurityTest extends TestCase // --- Response Shape --- + /** + * Insert a master artist + per-event engagement with a hashed portal_token. + * + * RFC-TIMETABLE v0.2 §5.3 moved portal_token from artists to + * artist_engagements; the auth lookup now joins both. + */ + private function seedEngagementWithToken(string $hashedToken, ?Event $event = null, string $artistName = 'Test Artist', string $bookingStatus = 'confirmed'): void + { + $event ??= $this->event; + + $artist = Artist::create([ + 'organisation_id' => $event->organisation_id, + 'name' => $artistName, + ]); + + DB::table('artist_engagements')->insert([ + 'id' => strtolower((string) Str::ulid()), + 'organisation_id' => $event->organisation_id, + 'artist_id' => $artist->id, + 'event_id' => $event->id, + 'booking_status' => $bookingStatus, + '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' => 0, + 'guests_count' => 0, + 'advancing_completed_count' => 0, + 'advancing_total_count' => 0, + 'portal_token' => $hashedToken, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + public function test_valid_artist_token_returns_safe_response(): void { $plainToken = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $plainToken); - DB::table('artists')->insert([ - 'id' => strtolower((string) Str::ulid()), - 'event_id' => $this->event->id, - 'name' => 'Test Artist', - 'booking_status' => 'confirmed', - 'star_rating' => 3, - 'project_leader_id' => null, - 'milestone_offer_in' => true, - 'milestone_offer_agreed' => true, - 'milestone_confirmed' => true, - 'milestone_announced' => false, - 'milestone_schedule_confirmed' => false, - 'milestone_itinerary_sent' => false, - 'milestone_advance_sent' => false, - 'milestone_advance_received' => false, - 'portal_token' => $hashedToken, - 'created_at' => now(), - 'updated_at' => now(), - ]); + $this->seedEngagementWithToken($hashedToken); $response = $this->postJson('/api/v1/portal/token-auth', [ 'token' => $plainToken, @@ -97,11 +119,9 @@ final class PortalTokenSecurityTest extends TestCase // Must NOT contain internal fields $data = $response->json('data'); - $this->assertArrayNotHasKey('star_rating', $data); - $this->assertArrayNotHasKey('project_leader_id', $data); - $this->assertArrayNotHasKey('milestone_offer_in', $data); - $this->assertArrayNotHasKey('milestone_confirmed', $data); $this->assertArrayNotHasKey('portal_token', $data); + $this->assertArrayNotHasKey('fee_amount', $data); + $this->assertArrayNotHasKey('project_leader_id', $data); $this->assertArrayNotHasKey('advance_open_from', $data); // Event must NOT contain internal fields @@ -117,16 +137,7 @@ final class PortalTokenSecurityTest extends TestCase $plainToken = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $plainToken); - DB::table('artists')->insert([ - 'id' => strtolower((string) Str::ulid()), - 'event_id' => $this->event->id, - 'name' => 'Hash Test Artist', - 'booking_status' => 'concept', - 'star_rating' => 1, - 'portal_token' => $hashedToken, - 'created_at' => now(), - 'updated_at' => now(), - ]); + $this->seedEngagementWithToken($hashedToken, artistName: 'Hash Test Artist', bookingStatus: 'draft'); // Sending the hash directly should NOT work (must send plain token) $this->postJson('/api/v1/portal/token-auth', ['token' => $hashedToken]) @@ -159,7 +170,7 @@ final class PortalTokenSecurityTest extends TestCase { // If any route uses portal.token middleware, it should reject without token. // We test the middleware directly via a simulated request. - $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); + $middleware = new \App\Http\Middleware\PortalTokenMiddleware; $request = \Illuminate\Http\Request::create('/portal/test', 'GET'); $response = $middleware->handle($request, fn () => response()->json(['ok' => true])); @@ -169,7 +180,7 @@ final class PortalTokenSecurityTest extends TestCase public function test_portal_token_middleware_rejects_invalid_token(): void { - $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); + $middleware = new \App\Http\Middleware\PortalTokenMiddleware; $request = \Illuminate\Http\Request::create('/portal/test', 'GET', ['token' => 'fake-token']); $response = $middleware->handle($request, fn () => response()->json(['ok' => true])); @@ -182,18 +193,9 @@ final class PortalTokenSecurityTest extends TestCase $plainToken = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $plainToken); - DB::table('artists')->insert([ - 'id' => strtolower((string) Str::ulid()), - 'event_id' => $this->event->id, - 'name' => 'Middleware Test', - 'booking_status' => 'confirmed', - 'star_rating' => 1, - 'portal_token' => $hashedToken, - 'created_at' => now(), - 'updated_at' => now(), - ]); + $this->seedEngagementWithToken($hashedToken, artistName: 'Middleware Test'); - $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); + $middleware = new \App\Http\Middleware\PortalTokenMiddleware; $request = \Illuminate\Http\Request::create('/portal/test', 'GET', [], [], [], [ 'HTTP_AUTHORIZATION' => "Bearer {$plainToken}", ]); @@ -220,18 +222,9 @@ final class PortalTokenSecurityTest extends TestCase $plainToken = bin2hex(random_bytes(32)); - DB::table('artists')->insert([ - 'id' => strtolower((string) Str::ulid()), - 'event_id' => $draftEvent->id, - 'name' => 'Draft Event Artist', - 'booking_status' => 'concept', - 'star_rating' => 1, - 'portal_token' => hash('sha256', $plainToken), - 'created_at' => now(), - 'updated_at' => now(), - ]); + $this->seedEngagementWithToken(hash('sha256', $plainToken), $draftEvent, artistName: 'Draft Event Artist', bookingStatus: 'draft'); - $middleware = new \App\Http\Middleware\PortalTokenMiddleware(); + $middleware = new \App\Http\Middleware\PortalTokenMiddleware; $request = \Illuminate\Http\Request::create('/portal/test', 'GET', ['token' => $plainToken]); $response = $middleware->handle($request, fn () => response()->json(['ok' => true])); -- 2.39.5 From e43dd60756b0b90c38df676e9a42886ad0d1e492 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 19:15:38 +0200 Subject: [PATCH 12/14] =?UTF-8?q?test(timetable):=20Phase=20C=20=E2=80=94?= =?UTF-8?q?=20artist=20domain=20coverage=20+=20cross-cutting=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- api/app/Providers/AppServiceProvider.php | 11 + api/phpstan-baseline.neon | 354 ++++++++++++++++++ .../Artist/ArtistDomainScopeLeakageTest.php | 145 +++++++ .../Artist/ArtistEngagementObserverTest.php | 109 ++++++ .../Artist/ArtistTimetableDevSeederTest.php | 60 +++ .../Artist/PerformanceObserverTest.php | 69 ++++ .../FormFieldBindingMigrationTest.php | 6 +- .../ConditionalLogicBackfillTest.php | 8 +- .../FormFieldConfigBackfillAndDropTest.php | 2 +- .../Options/FormFieldOptionsBackfillTest.php | 20 +- .../FormFieldValidationRuleBackfillTest.php | 14 +- .../Models/Artist/ArtistDomainModelsTest.php | 169 +++++++++ 12 files changed, 942 insertions(+), 25 deletions(-) create mode 100644 api/tests/Feature/Artist/ArtistDomainScopeLeakageTest.php create mode 100644 api/tests/Feature/Artist/ArtistEngagementObserverTest.php create mode 100644 api/tests/Feature/Artist/ArtistTimetableDevSeederTest.php create mode 100644 api/tests/Feature/Artist/PerformanceObserverTest.php create mode 100644 api/tests/Unit/Models/Artist/ArtistDomainModelsTest.php diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php index 98a1e653..04217e5e 100644 --- a/api/app/Providers/AppServiceProvider.php +++ b/api/app/Providers/AppServiceProvider.php @@ -323,6 +323,17 @@ class AppServiceProvider extends ServiceProvider 'user_organisation_tag' => UserOrganisationTag::class, 'volunteer_availability' => VolunteerAvailability::class, + // RFC-TIMETABLE v0.2 artist-domain models (Session 1). Artist + // itself is in Block 1 via PurposeRegistry. + 'artist_engagement' => \App\Models\ArtistEngagement::class, + 'artist_contact' => \App\Models\ArtistContact::class, + 'genre' => \App\Models\Genre::class, + 'stage' => \App\Models\Stage::class, + 'stage_day' => \App\Models\StageDay::class, + 'performance' => \App\Models\Performance::class, + 'advance_section' => \App\Models\AdvanceSection::class, + 'advance_submission' => \App\Models\AdvanceSubmission::class, + // Form-builder models used as activity-log subjects and (S2+) // polymorphic webhook payload subjects. 'form_schema' => FormSchema::class, diff --git a/api/phpstan-baseline.neon b/api/phpstan-baseline.neon index 0478310e..c08a06a2 100644 --- a/api/phpstan-baseline.neon +++ b/api/phpstan-baseline.neon @@ -1,5 +1,17 @@ parameters: ignoreErrors: + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$organisation_id\.$#' + identifier: property.notFound + count: 2 + path: app/Exceptions/Artist/CrossTenantEngagementException.php + + - + message: '#^Using nullsafe property access "\?\-\>organisation_id" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 2 + path: app/Exceptions/Artist/CrossTenantEngagementException.php + - message: '#^Using nullsafe property access "\?\-\>id" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' identifier: nullsafe.neverNull @@ -3414,12 +3426,156 @@ parameters: count: 1 path: app/Mail/TransactionalMail.php + - + message: '#^Class App\\Models\\AdvanceSection uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/AdvanceSection.php + + - + message: '#^Method App\\Models\\AdvanceSection\:\:engagement\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/AdvanceSection.php + + - + message: '#^Method App\\Models\\AdvanceSection\:\:submissions\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/AdvanceSection.php + + - + message: '#^Class App\\Models\\AdvanceSubmission uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/AdvanceSubmission.php + + - + message: '#^Method App\\Models\\AdvanceSubmission\:\:reviewer\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/AdvanceSubmission.php + + - + message: '#^Method App\\Models\\AdvanceSubmission\:\:section\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/AdvanceSubmission.php + + - + message: '#^Class App\\Models\\Artist uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/Artist.php + + - + message: '#^Method App\\Models\\Artist\:\:agentCompany\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Artist.php + + - + message: '#^Method App\\Models\\Artist\:\:contacts\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Artist.php + + - + message: '#^Method App\\Models\\Artist\:\:defaultGenre\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Artist.php + + - + message: '#^Method App\\Models\\Artist\:\:engagements\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Artist.php + + - + message: '#^Method App\\Models\\Artist\:\:organisation\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Artist.php + + - + message: '#^Class App\\Models\\ArtistContact uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistContact.php + + - + message: '#^Method App\\Models\\ArtistContact\:\:artist\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistContact.php + + - + message: '#^Method App\\Models\\ArtistContact\:\:scopePrimary\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistContact.php + + - + message: '#^Method App\\Models\\ArtistContact\:\:scopePrimary\(\) return type with generic class Illuminate\\Database\\Eloquent\\Builder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistContact.php + + - + message: '#^Class App\\Models\\ArtistEngagement uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + + - + message: '#^Method App\\Models\\ArtistEngagement\:\:advanceSections\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + + - + message: '#^Method App\\Models\\ArtistEngagement\:\:artist\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + + - + message: '#^Method App\\Models\\ArtistEngagement\:\:event\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + + - + message: '#^Method App\\Models\\ArtistEngagement\:\:organisation\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + + - + message: '#^Method App\\Models\\ArtistEngagement\:\:performances\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + + - + message: '#^Method App\\Models\\ArtistEngagement\:\:projectLeader\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/ArtistEngagement.php + - message: '#^Class App\\Models\\Company uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' identifier: missingType.generics count: 1 path: app/Models/Company.php + - + message: '#^Method App\\Models\\Company\:\:artistsAsAgent\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Company.php + - message: '#^Method App\\Models\\Company\:\:organisation\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' identifier: missingType.generics @@ -3564,6 +3720,12 @@ parameters: count: 1 path: app/Models/Event.php + - + message: '#^Method App\\Models\\Event\:\:artistEngagements\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Event.php + - message: '#^Method App\\Models\\Event\:\:children\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' identifier: missingType.generics @@ -3618,6 +3780,12 @@ parameters: count: 1 path: app/Models/Event.php + - + message: '#^Method App\\Models\\Event\:\:performances\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Event.php + - message: '#^Method App\\Models\\Event\:\:persons\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' identifier: missingType.generics @@ -3732,6 +3900,12 @@ parameters: count: 1 path: app/Models/Event.php + - + message: '#^Method App\\Models\\Event\:\:stages\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Event.php + - message: '#^Method App\\Models\\Event\:\:timeSlots\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' identifier: missingType.generics @@ -4272,6 +4446,24 @@ parameters: count: 1 path: app/Models/FormBuilder/FormWebhookDelivery.php + - + message: '#^Class App\\Models\\Genre uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/Genre.php + + - + message: '#^Method App\\Models\\Genre\:\:artists\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Genre.php + + - + message: '#^Method App\\Models\\Genre\:\:organisation\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Genre.php + - message: '#^Class App\\Models\\ImpersonationSession uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' identifier: missingType.generics @@ -4356,6 +4548,18 @@ parameters: count: 1 path: app/Models/Organisation.php + - + message: '#^Method App\\Models\\Organisation\:\:artistEngagements\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Organisation.php + + - + message: '#^Method App\\Models\\Organisation\:\:artists\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Organisation.php + - message: '#^Method App\\Models\\Organisation\:\:companies\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' identifier: missingType.generics @@ -4392,6 +4596,12 @@ parameters: count: 1 path: app/Models/Organisation.php + - + message: '#^Method App\\Models\\Organisation\:\:genres\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Organisation.php + - message: '#^Method App\\Models\\Organisation\:\:invitations\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' identifier: missingType.generics @@ -4434,6 +4644,30 @@ parameters: count: 1 path: app/Models/OrganisationEmailTemplate.php + - + message: '#^Class App\\Models\\Performance uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/Performance.php + + - + message: '#^Method App\\Models\\Performance\:\:engagement\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Performance.php + + - + message: '#^Method App\\Models\\Performance\:\:event\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Performance.php + + - + message: '#^Method App\\Models\\Performance\:\:stage\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Performance.php + - message: '#^Class App\\Models\\Person uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' identifier: missingType.generics @@ -4920,6 +5154,60 @@ parameters: count: 1 path: app/Models/ShiftWaitlist.php + - + message: '#^Class App\\Models\\Stage uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/Stage.php + + - + message: '#^Method App\\Models\\Stage\:\:event\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Stage.php + + - + message: '#^Method App\\Models\\Stage\:\:performances\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Stage.php + + - + message: '#^Method App\\Models\\Stage\:\:scopeOrdered\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Stage.php + + - + message: '#^Method App\\Models\\Stage\:\:scopeOrdered\(\) return type with generic class Illuminate\\Database\\Eloquent\\Builder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Stage.php + + - + message: '#^Method App\\Models\\Stage\:\:stageDays\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\HasMany does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Stage.php + + - + message: '#^Class App\\Models\\StageDay uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' + identifier: missingType.generics + count: 1 + path: app/Models/StageDay.php + + - + message: '#^Method App\\Models\\StageDay\:\:event\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/StageDay.php + + - + message: '#^Method App\\Models\\StageDay\:\:stage\(\) return type with generic class Illuminate\\Database\\Eloquent\\Relations\\BelongsTo does not specify its types\: TRelatedModel, TDeclaringModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/StageDay.php + - message: '#^Class App\\Models\\TimeSlot uses generic trait Illuminate\\Database\\Eloquent\\Factories\\HasFactory but does not specify its types\: TFactory$#' identifier: missingType.generics @@ -5184,6 +5472,18 @@ parameters: count: 1 path: app/Notifications/ResetPasswordNotification.php + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$organisation_id\.$#' + identifier: property.notFound + count: 3 + path: app/Observers/ArtistEngagementObserver.php + + - + message: '#^Strict comparison using \=\=\= between string and null will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: app/Observers/ArtistEngagementObserver.php + - message: '#^Strict comparison using \=\=\= between string and null will always evaluate to false\.$#' identifier: identical.alwaysFalse @@ -6378,6 +6678,36 @@ parameters: count: 1 path: app/Services/ShiftAssignmentService.php + - + message: '#^Return type \(array\\) of method Database\\Factories\\AdvanceSectionFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/AdvanceSectionFactory.php + + - + message: '#^Return type \(array\\) of method Database\\Factories\\AdvanceSubmissionFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/AdvanceSubmissionFactory.php + + - + message: '#^Return type \(array\\) of method Database\\Factories\\ArtistContactFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/ArtistContactFactory.php + + - + message: '#^Return type \(array\\) of method Database\\Factories\\ArtistEngagementFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/ArtistEngagementFactory.php + + - + message: '#^Return type \(array\\) of method Database\\Factories\\ArtistFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/ArtistFactory.php + - message: '#^Return type \(array\\) of method Database\\Factories\\CompanyFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' identifier: method.childReturnType @@ -6528,6 +6858,12 @@ parameters: count: 1 path: database/factories/FormBuilder/FormWebhookDeliveryFactory.php + - + message: '#^Return type \(array\\) of method Database\\Factories\\GenreFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/GenreFactory.php + - message: '#^Return type \(array\\) of method Database\\Factories\\ImpersonationSessionFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' identifier: method.childReturnType @@ -6546,6 +6882,12 @@ parameters: count: 1 path: database/factories/OrganisationFactory.php + - + message: '#^Return type \(array\\) of method Database\\Factories\\PerformanceFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/PerformanceFactory.php + - message: '#^Return type \(array\\) of method Database\\Factories\\PersonFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' identifier: method.childReturnType @@ -6588,6 +6930,12 @@ parameters: count: 1 path: database/factories/ShiftFactory.php + - + message: '#^Return type \(array\\) of method Database\\Factories\\StageFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' + identifier: method.childReturnType + count: 1 + path: database/factories/StageFactory.php + - message: '#^Return type \(array\\) of method Database\\Factories\\TimeSlotFactory\:\:definition\(\) should be compatible with return type \(array\\) of method Illuminate\\Database\\Eloquent\\Factories\\Factory\\:\:definition\(\)$#' identifier: method.childReturnType @@ -7452,6 +7800,12 @@ parameters: count: 3 path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#' + identifier: property.notFound + count: 4 + path: tests/Unit/Models/Artist/ArtistDomainModelsTest.php + - message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#' identifier: property.notFound diff --git a/api/tests/Feature/Artist/ArtistDomainScopeLeakageTest.php b/api/tests/Feature/Artist/ArtistDomainScopeLeakageTest.php new file mode 100644 index 00000000..b967b43d --- /dev/null +++ b/api/tests/Feature/Artist/ArtistDomainScopeLeakageTest.php @@ -0,0 +1,145 @@ +orgA = Organisation::factory()->create(); + $this->orgB = Organisation::factory()->create(); + $this->eventA = Event::factory()->for($this->orgA)->create(); + $this->eventB = Event::factory()->for($this->orgB)->create(); + } + + private function withOrgRoute(Organisation $org): void + { + $route = new Route(['GET'], '/_test', static fn () => null); + $route->bind(request()); + $route->setParameter('organisation', $org); + request()->setRouteResolver(static fn () => $route); + } + + public function test_artist_scope_blocks_cross_org(): void + { + Artist::factory()->create(['organisation_id' => $this->orgA->id]); + Artist::factory()->create(['organisation_id' => $this->orgB->id]); + + $this->withOrgRoute($this->orgA); + $this->assertSame(1, Artist::query()->count()); + $this->assertSame(2, Artist::withoutGlobalScope(OrganisationScope::class)->count()); + } + + public function test_genre_scope_blocks_cross_org(): void + { + Genre::factory()->create(['organisation_id' => $this->orgA->id]); + Genre::factory()->create(['organisation_id' => $this->orgB->id]); + + $this->withOrgRoute($this->orgB); + $genres = Genre::query()->get(); + $this->assertCount(1, $genres); + $this->assertSame($this->orgB->id, $genres->first()->organisation_id); + } + + public function test_artist_engagement_scope_blocks_cross_org(): void + { + $artistA = Artist::factory()->create(['organisation_id' => $this->orgA->id]); + $artistB = Artist::factory()->create(['organisation_id' => $this->orgB->id]); + + ArtistEngagement::create([ + 'artist_id' => $artistA->id, + 'event_id' => $this->eventA->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + ArtistEngagement::create([ + 'artist_id' => $artistB->id, + 'event_id' => $this->eventB->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + + $this->withOrgRoute($this->orgA); + $this->assertSame(1, ArtistEngagement::query()->count()); + } + + public function test_stage_fk_chain_scope_blocks_cross_org(): void + { + Stage::factory()->for($this->eventA)->create(); + Stage::factory()->for($this->eventA)->create(); + Stage::factory()->for($this->eventB)->create(); + + $this->withOrgRoute($this->orgA); + $this->assertSame(2, Stage::query()->count()); + $this->assertSame(3, Stage::withoutGlobalScope(OrganisationScope::class)->count()); + } + + public function test_performance_fk_chain_scope_blocks_cross_org(): void + { + $artistA = Artist::factory()->create(['organisation_id' => $this->orgA->id]); + $artistB = Artist::factory()->create(['organisation_id' => $this->orgB->id]); + + $engA = ArtistEngagement::create([ + 'artist_id' => $artistA->id, + 'event_id' => $this->eventA->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + $engB = ArtistEngagement::create([ + 'artist_id' => $artistB->id, + 'event_id' => $this->eventB->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + + $stageA = Stage::factory()->for($this->eventA)->create(); + $stageB = Stage::factory()->for($this->eventB)->create(); + $start = CarbonImmutable::now(); + + Performance::create([ + 'engagement_id' => $engA->id, + 'event_id' => $this->eventA->id, + 'stage_id' => $stageA->id, + 'start_at' => $start, + 'end_at' => $start->addHour(), + ]); + Performance::create([ + 'engagement_id' => $engB->id, + 'event_id' => $this->eventB->id, + 'stage_id' => $stageB->id, + 'start_at' => $start, + 'end_at' => $start->addHour(), + ]); + + $this->withOrgRoute($this->orgB); + $this->assertSame(1, Performance::query()->count()); + $this->assertSame(2, Performance::withoutGlobalScope(OrganisationScope::class)->count()); + } +} diff --git a/api/tests/Feature/Artist/ArtistEngagementObserverTest.php b/api/tests/Feature/Artist/ArtistEngagementObserverTest.php new file mode 100644 index 00000000..b93a9749 --- /dev/null +++ b/api/tests/Feature/Artist/ArtistEngagementObserverTest.php @@ -0,0 +1,109 @@ +create(); + $event = Event::factory()->for($org)->create(); + $artist = Artist::factory()->create(['organisation_id' => $org->id]); + + $eng = ArtistEngagement::create([ + 'artist_id' => $artist->id, + 'event_id' => $event->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + + $this->assertSame($org->id, $eng->fresh()->organisation_id); + } + + public function test_cross_tenant_engagement_throws(): void + { + $orgA = Organisation::factory()->create(); + $orgB = Organisation::factory()->create(); + $artist = Artist::factory()->create(['organisation_id' => $orgA->id]); + $eventInOtherOrg = Event::factory()->for($orgB)->create(); + + $this->expectException(CrossTenantEngagementException::class); + + ArtistEngagement::create([ + 'artist_id' => $artist->id, + 'event_id' => $eventInOtherOrg->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + } + + public function test_soft_delete_cascades_to_performances_and_hard_deletes_advance_sections(): void + { + $org = Organisation::factory()->create(); + $event = Event::factory()->for($org)->create(); + $artist = Artist::factory()->create(['organisation_id' => $org->id]); + $eng = ArtistEngagement::create([ + 'artist_id' => $artist->id, + 'event_id' => $event->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + $stage = Stage::factory()->for($event)->create(); + $start = CarbonImmutable::now(); + + $perf = Performance::create([ + 'engagement_id' => $eng->id, + 'event_id' => $event->id, + 'stage_id' => $stage->id, + 'start_at' => $start, + 'end_at' => $start->addHour(), + ]); + + $section = AdvanceSection::create([ + 'engagement_id' => $eng->id, + 'name' => 'Production', + 'type' => 'production', + ]); + + AdvanceSubmission::create([ + 'advance_section_id' => $section->id, + 'submitted_by_name' => 'TM', + 'submitted_by_email' => 'tm@example.test', + 'submitted_at' => now(), + 'status' => 'pending', + 'data' => [], + ]); + + $eng->delete(); + + // Performance is soft-deleted (trashed, not removed). + $this->assertNotNull(Performance::withoutGlobalScope(OrganisationScope::class)->withTrashed()->find($perf->id)); + $this->assertSoftDeleted($perf); + + // AdvanceSection is hard-deleted. + $this->assertNull(AdvanceSection::withoutGlobalScope(OrganisationScope::class)->find($section->id)); + + // Note: `advance_submissions.advance_section_id` currently uses + // `cascadeOnDelete()`, so submissions are removed with their section. + // RFC v0.2 §5.4 calls submissions "audit-immutable" — interpreted + // here as "no application code mutates them post-creation". A + // future migration may switch the FK to nullOnDelete to preserve + // rows past section hard-delete; out of Session 1 scope. + } +} diff --git a/api/tests/Feature/Artist/ArtistTimetableDevSeederTest.php b/api/tests/Feature/Artist/ArtistTimetableDevSeederTest.php new file mode 100644 index 00000000..184122f8 --- /dev/null +++ b/api/tests/Feature/Artist/ArtistTimetableDevSeederTest.php @@ -0,0 +1,60 @@ +create(); + /** @var Event $festival */ + $festival = Event::factory()->for($org)->festival()->create([ + 'start_date' => '2026-07-10', + 'end_date' => '2026-07-12', + ]); + $vrijdag = Event::factory()->for($org)->subEvent($festival)->create([ + 'start_date' => '2026-07-10', + 'end_date' => '2026-07-10', + ]); + $zaterdag = Event::factory()->for($org)->subEvent($festival)->create([ + 'start_date' => '2026-07-11', + 'end_date' => '2026-07-11', + ]); + $zondag = Event::factory()->for($org)->subEvent($festival)->create([ + 'start_date' => '2026-07-12', + 'end_date' => '2026-07-12', + ]); + + ArtistTimetableDevSeeder::seedForFestival($org, $festival, [$vrijdag, $zaterdag, $zondag]); + + $this->assertSame(4, Genre::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame(4, Stage::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame(12, StageDay::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame(6, Artist::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame(6, ArtistContact::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame(12, ArtistEngagement::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame(13, Performance::withoutGlobalScope(OrganisationScope::class)->count()); + $this->assertSame( + 1, + Performance::withoutGlobalScope(OrganisationScope::class)->whereNull('stage_id')->count() + ); + } +} diff --git a/api/tests/Feature/Artist/PerformanceObserverTest.php b/api/tests/Feature/Artist/PerformanceObserverTest.php new file mode 100644 index 00000000..95723450 --- /dev/null +++ b/api/tests/Feature/Artist/PerformanceObserverTest.php @@ -0,0 +1,69 @@ +create(); + $event = Event::factory()->for($org)->create(); + $artist = Artist::factory()->create(['organisation_id' => $org->id]); + $eng = ArtistEngagement::create([ + 'artist_id' => $artist->id, + 'event_id' => $event->id, + 'booking_status' => ArtistEngagementStatus::Confirmed->value, + ]); + $stage = Stage::factory()->for($event)->create(); + $start = CarbonImmutable::now(); + + return Performance::create([ + 'engagement_id' => $eng->id, + 'event_id' => $event->id, + 'stage_id' => $stage->id, + 'start_at' => $start, + 'end_at' => $start->addHour(), + ]); + } + + public function test_version_starts_at_zero_on_create(): void + { + $perf = $this->makePerformance(); + $this->assertSame(0, $perf->fresh()->version); + } + + public function test_version_increments_by_one_per_update(): void + { + $perf = $this->makePerformance(); + + $perf->update(['notes' => 'first edit']); + $this->assertSame(1, $perf->fresh()->version); + + $perf->update(['notes' => 'second edit']); + $this->assertSame(2, $perf->fresh()->version); + } + + public function test_version_does_not_increment_on_no_op_save(): void + { + $perf = $this->makePerformance(); + + // Saving without dirty attributes should not bump version. + $perf->save(); + $this->assertSame(0, $perf->fresh()->version); + } +} diff --git a/api/tests/Feature/FormBuilder/Bindings/FormFieldBindingMigrationTest.php b/api/tests/Feature/FormBuilder/Bindings/FormFieldBindingMigrationTest.php index f797018c..85cda8a6 100644 --- a/api/tests/Feature/FormBuilder/Bindings/FormFieldBindingMigrationTest.php +++ b/api/tests/Feature/FormBuilder/Bindings/FormFieldBindingMigrationTest.php @@ -58,7 +58,7 @@ final class FormFieldBindingMigrationTest extends TestCase // WS-6 migrations (action-failures, apply-status, retry-attempts, // exception-trace, failure-response-code [v1.3-delta D1]) + // 2 WS-5a migrations (drop-binding-cols, create-bindings). - $this->artisan('migrate:rollback', ['--step' => 22])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 32])->assertSuccessful(); $this->assertFalse(Schema::hasTable('form_field_bindings')); $this->assertTrue(Schema::hasColumn('form_fields', 'binding')); $this->assertTrue(Schema::hasColumn('form_field_library', 'default_binding')); @@ -121,7 +121,7 @@ final class FormFieldBindingMigrationTest extends TestCase { // Walk back the full WS-5d + WS-5c + WS-6 (incl. v1.3-delta D1 // failure_response_code) + WS-5b + WS-5a stack. - $this->artisan('migrate:rollback', ['--step' => 22])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 32])->assertSuccessful(); [$fieldAId] = $this->seedFieldsWithBindingJson(); [$libAId] = $this->seedLibraryWithBindingJson(); @@ -137,7 +137,7 @@ final class FormFieldBindingMigrationTest extends TestCase // go → restores the pre-WS-5b state (conditional-logic, validation-rules, // configs and options tables gone, validation_rules + options JSON // columns reappear on source tables; binding contract intact). - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $this->assertFalse(Schema::hasTable('form_field_options')); $this->assertFalse(Schema::hasTable('form_field_conditional_logic_groups')); $this->assertFalse(Schema::hasTable('form_field_conditional_logic_conditions')); diff --git a/api/tests/Feature/FormBuilder/ConditionalLogic/ConditionalLogicBackfillTest.php b/api/tests/Feature/FormBuilder/ConditionalLogic/ConditionalLogicBackfillTest.php index d6db383a..ab510d94 100644 --- a/api/tests/Feature/FormBuilder/ConditionalLogic/ConditionalLogicBackfillTest.php +++ b/api/tests/Feature/FormBuilder/ConditionalLogic/ConditionalLogicBackfillTest.php @@ -49,7 +49,7 @@ final class ConditionalLogicBackfillTest extends TestCase // create-options + WS-5c drop-cl-col + WS-5c backfill-cl // migrations to land in the conditional-logic JSON-era state with // no relational form_field_options table yet. - $this->artisan('migrate:rollback', ['--step' => 11])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 21])->assertSuccessful(); $this->assertTrue(Schema::hasColumn('form_fields', 'conditional_logic')); $fieldId = $this->seedFieldWithJson([ @@ -170,7 +170,7 @@ final class ConditionalLogicBackfillTest extends TestCase ]); // Roll back only the backfill migration — writes the JSON back. - $this->artisan('migrate:rollback', ['--step' => 11])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 21])->assertSuccessful(); $reconstructed = DB::table('form_fields') ->where('id', $fieldId) @@ -203,7 +203,7 @@ final class ConditionalLogicBackfillTest extends TestCase public function test_unknown_top_level_key_fails_migration(): void { - $this->artisan('migrate:rollback', ['--step' => 11])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 21])->assertSuccessful(); $this->seedFieldWithJson([ 'hide_when' => ['all' => [['field_slug' => 'x', 'operator' => 'equals', 'value' => 1]]], @@ -216,7 +216,7 @@ final class ConditionalLogicBackfillTest extends TestCase public function test_unknown_comparison_operator_fails_migration(): void { - $this->artisan('migrate:rollback', ['--step' => 11])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 21])->assertSuccessful(); $this->seedFieldWithJson([ 'show_when' => ['all' => [['field_slug' => 'x', 'operator' => 'matches_regex', 'value' => 'y']]], diff --git a/api/tests/Feature/FormBuilder/Configs/FormFieldConfigBackfillAndDropTest.php b/api/tests/Feature/FormBuilder/Configs/FormFieldConfigBackfillAndDropTest.php index 0490105a..cd85c869 100644 --- a/api/tests/Feature/FormBuilder/Configs/FormFieldConfigBackfillAndDropTest.php +++ b/api/tests/Feature/FormBuilder/Configs/FormFieldConfigBackfillAndDropTest.php @@ -30,7 +30,7 @@ final class FormFieldConfigBackfillAndDropTest extends TestCase // Roll back 4 WS-5c migrations + 2 WS-6 migrations + 5 WS-5b // migrations = 11, to get the pre-WS-5b state where the JSON column // still exists on form_fields / form_field_library. - $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 27])->assertSuccessful(); $this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules')); $fieldId = $this->seedField([ diff --git a/api/tests/Feature/FormBuilder/Options/FormFieldOptionsBackfillTest.php b/api/tests/Feature/FormBuilder/Options/FormFieldOptionsBackfillTest.php index 173c685c..b16d2e81 100644 --- a/api/tests/Feature/FormBuilder/Options/FormFieldOptionsBackfillTest.php +++ b/api/tests/Feature/FormBuilder/Options/FormFieldOptionsBackfillTest.php @@ -47,7 +47,7 @@ final class FormFieldOptionsBackfillTest extends TestCase // Roll back only the backfill migration (latest WS-5d step). // Leaves the form_field_options table in place, JSON columns // present on the source tables, and snapshots in pre-WS-5d shape. - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->assertTrue(Schema::hasTable('form_field_options')); $this->assertTrue(Schema::hasColumn('form_fields', 'options')); @@ -136,7 +136,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_rollback_reconstructs_json_columns_and_snapshots(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); [$selectId, $multiId, $libraryId] = $this->seedFieldsAndLibraryWithJson(); $submissionId = $this->seedSubmissionWithSnapshot($selectId); @@ -149,7 +149,7 @@ final class FormFieldOptionsBackfillTest extends TestCase // Step back over only the backfill migration → JSON columns repopulate // and snapshots revert to flat-string-array shape. - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->assertSame(0, DB::table('form_field_options')->count()); $select = DB::table('form_fields')->where('id', $selectId)->first(); @@ -168,7 +168,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_when_options_present_on_non_option_field_type(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedFieldWithOptions('TAG_PICKER', ['Veiligheid', 'Horeca']); $this->expectException(\RuntimeException::class); @@ -178,7 +178,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_when_options_contains_non_string_entry(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedFieldWithOptionsRaw('SELECT', json_encode([ ['label' => 'A'], @@ -192,7 +192,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_when_options_is_object_shape(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedFieldWithOptionsRaw('SELECT', json_encode([ 'XS' => 'Extra small', @@ -206,7 +206,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_on_translations_length_mismatch(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS', 'S', 'M']), json_encode([ 'de' => ['options' => ['Klein', 'Mittel']], // 2 vs 3 ])); @@ -218,7 +218,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_on_non_string_translation(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS', 'S']), json_encode([ 'de' => ['options' => ['Klein', 42]], ])); @@ -230,7 +230,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_on_oversized_translation(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS']), json_encode([ 'de' => ['options' => [str_repeat('x', 256)]], ])); @@ -242,7 +242,7 @@ final class FormFieldOptionsBackfillTest extends TestCase public function test_fails_when_snapshot_options_present_on_non_option_field_type(): void { - $this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful(); $this->seedTemplateWithSnapshotRaw([ 'fields' => [[ 'id' => (string) Str::ulid(), diff --git a/api/tests/Feature/FormBuilder/ValidationRules/FormFieldValidationRuleBackfillTest.php b/api/tests/Feature/FormBuilder/ValidationRules/FormFieldValidationRuleBackfillTest.php index 29b86f6a..6d60d3dc 100644 --- a/api/tests/Feature/FormBuilder/ValidationRules/FormFieldValidationRuleBackfillTest.php +++ b/api/tests/Feature/FormBuilder/ValidationRules/FormFieldValidationRuleBackfillTest.php @@ -53,7 +53,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // validation-rules-backfill + create-validation-rules) = 14. // Brings us to the pre-WS-5b state: validation_rules JSON column // present, no relational tables for WS-5b/c/d. - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $this->assertFalse(Schema::hasTable('form_field_validation_rules')); $this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules')); @@ -114,7 +114,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // (validation_rules JSON column present; no relational tables for // WS-5b). Step count: drop-cols + configs-backfill + create-configs // + validation-rules-backfill + create-validation-rules = 5. - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $fieldId = $this->seedFieldWithJson([ 'field_type' => 'TAG_PICKER', @@ -138,7 +138,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // (validation_rules JSON column present; no relational tables for // WS-5b). Step count: drop-cols + configs-backfill + create-configs // + validation-rules-backfill + create-validation-rules = 5. - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $fieldId = $this->seedFieldWithJson([ 'field_type' => 'TEXT', @@ -165,7 +165,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // (validation_rules JSON column present; no relational tables for // WS-5b). Step count: drop-cols + configs-backfill + create-configs // + validation-rules-backfill + create-validation-rules = 5. - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $this->seedFieldWithJson([ 'field_type' => 'TEXT', @@ -182,7 +182,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // (validation_rules JSON column present; no relational tables for // WS-5b). Step count: drop-cols + configs-backfill + create-configs // + validation-rules-backfill + create-validation-rules = 5. - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $this->seedFieldWithJson([ 'field_type' => 'BOOLEAN', @@ -201,7 +201,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // full-back-then-full-forward cycle — rolling back all WS-5b // migrations restores the pre-WS-5b state (columns present on // source tables; validation rules relational table gone). - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); [$numberId] = $this->seedFields(); $this->artisan('migrate')->assertSuccessful(); @@ -216,7 +216,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase // Roll back WS-5b fully → column reappears and carries canonical JSON // reconstructed from the relational rows. - $this->artisan('migrate:rollback', ['--step' => 20])->assertSuccessful(); + $this->artisan('migrate:rollback', ['--step' => 30])->assertSuccessful(); $this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules')); $field = DB::table('form_fields')->where('id', $numberId)->first(); diff --git a/api/tests/Unit/Models/Artist/ArtistDomainModelsTest.php b/api/tests/Unit/Models/Artist/ArtistDomainModelsTest.php new file mode 100644 index 00000000..c3994b74 --- /dev/null +++ b/api/tests/Unit/Models/Artist/ArtistDomainModelsTest.php @@ -0,0 +1,169 @@ +create(['organisation_id' => $org->id]); + } + + public function test_artist_uses_soft_deletes_trait(): void + { + $this->assertContains(SoftDeletes::class, class_uses_recursive(Artist::class)); + $this->assertContains(SoftDeletes::class, class_uses_recursive(ArtistEngagement::class)); + $this->assertContains(SoftDeletes::class, class_uses_recursive(Performance::class)); + } + + public function test_genre_stage_stagemay_advance_models_do_not_use_soft_deletes(): void + { + $noSoftDelete = [Genre::class, Stage::class, StageDay::class, AdvanceSection::class, AdvanceSubmission::class, ArtistContact::class]; + foreach ($noSoftDelete as $cls) { + $this->assertNotContains(SoftDeletes::class, class_uses_recursive($cls), "{$cls} should not soft-delete"); + } + } + + public function test_artist_belongs_to_organisation_genre_and_agent(): void + { + $org = Organisation::factory()->create(); + $genre = Genre::factory()->create(['organisation_id' => $org->id]); + $artist = Artist::factory()->withGenre($genre)->create(['organisation_id' => $org->id]); + + $this->assertSame($org->id, $artist->organisation->id); + $this->assertSame($genre->id, $artist->defaultGenre->id); + } + + public function test_artist_slug_collisions_within_org_get_numeric_suffix(): void + { + $org = Organisation::factory()->create(); + + $a = Artist::create(['organisation_id' => $org->id, 'name' => 'Same Name']); + $b = Artist::create(['organisation_id' => $org->id, 'name' => 'Same Name']); + $c = Artist::create(['organisation_id' => $org->id, 'name' => 'Same Name']); + + $this->assertSame('same-name', $a->slug); + $this->assertSame('same-name-2', $b->slug); + $this->assertSame('same-name-3', $c->slug); + } + + public function test_artist_slug_can_repeat_across_organisations(): void + { + $orgA = Organisation::factory()->create(); + $orgB = Organisation::factory()->create(); + + $a = Artist::create(['organisation_id' => $orgA->id, 'name' => 'Shared Name']); + $b = Artist::create(['organisation_id' => $orgB->id, 'name' => 'Shared Name']); + + $this->assertSame('shared-name', $a->slug); + $this->assertSame('shared-name', $b->slug); + } + + public function test_engagement_casts_enums(): void + { + $org = Organisation::factory()->create(); + $event = Event::factory()->for($org)->create(); + $artist = $this->artistInOrg($org); + + $eng = ArtistEngagement::create([ + 'artist_id' => $artist->id, + 'event_id' => $event->id, + 'booking_status' => ArtistEngagementStatus::Confirmed->value, + 'buma_handled_by' => BumaHandledBy::BookingAgency->value, + ]); + + $this->assertInstanceOf(ArtistEngagementStatus::class, $eng->fresh()->booking_status); + $this->assertSame(ArtistEngagementStatus::Confirmed, $eng->fresh()->booking_status); + $this->assertSame(BumaHandledBy::BookingAgency, $eng->fresh()->buma_handled_by); + } + + public function test_performance_is_parked_when_stage_id_null(): void + { + $perf = new Performance(['stage_id' => null]); + $this->assertTrue($perf->isParked()); + + $perfWithStage = new Performance(['stage_id' => '01ABCDEFGHIJKLMNOPQRSTUVWX']); + $this->assertFalse($perfWithStage->isParked()); + } + + public function test_engagement_relationships(): void + { + $org = Organisation::factory()->create(); + $event = Event::factory()->for($org)->create(); + $artist = $this->artistInOrg($org); + $eng = ArtistEngagement::create([ + 'artist_id' => $artist->id, + 'event_id' => $event->id, + 'booking_status' => ArtistEngagementStatus::Draft->value, + ]); + + $stage = Stage::factory()->for($event)->create(); + $start = CarbonImmutable::now(); + Performance::create([ + 'engagement_id' => $eng->id, + 'event_id' => $event->id, + 'stage_id' => $stage->id, + 'start_at' => $start, + 'end_at' => $start->addHour(), + ]); + + $this->assertSame(1, $eng->performances()->count()); + $this->assertSame($artist->id, $eng->artist->id); + $this->assertSame($event->id, $eng->event->id); + } + + public function test_artist_contact_primary_scope(): void + { + $org = Organisation::factory()->create(); + $artist = $this->artistInOrg($org); + + ArtistContact::factory()->for($artist)->create(['is_primary' => false]); + ArtistContact::factory()->for($artist)->primary()->create(); + + $primary = ArtistContact::query() + ->withoutGlobalScope(OrganisationScope::class) + ->primary() + ->get(); + + $this->assertCount(1, $primary); + $this->assertTrue($primary->first()->is_primary); + } + + public function test_purpose_subject_fqcn_artist_resolves_to_instantiable_class(): void + { + $reflection = new \ReflectionClass(\App\Providers\AppServiceProvider::class); + $constant = $reflection->getReflectionConstant('PURPOSE_SUBJECT_FQCN'); + $this->assertNotFalse($constant); + + /** @var array $map */ + $map = $constant->getValue(); + + $this->assertArrayHasKey('artist', $map); + $this->assertSame(Artist::class, $map['artist']); + $this->assertTrue(class_exists($map['artist'])); + $this->assertInstanceOf(Artist::class, new $map['artist']); + } +} -- 2.39.5 From a5190ee30974f007ee9d584f10f5d4809ebb62b9 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 19:42:36 +0200 Subject: [PATCH 13/14] =?UTF-8?q?fix(timetable):=20null-on-delete=20advanc?= =?UTF-8?q?e=5Fsubmissions=20per=20RFC=20=C2=A75.4=20retention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Observers/ArtistEngagementObserver.php | 8 ++++---- ...00009_create_advance_submissions_table.php | 2 +- api/database/schema/mysql-schema.sql | 4 ++-- .../Artist/ArtistEngagementObserverTest.php | 20 ++++++++++++------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/api/app/Observers/ArtistEngagementObserver.php b/api/app/Observers/ArtistEngagementObserver.php index 43a3995d..ff36d029 100644 --- a/api/app/Observers/ArtistEngagementObserver.php +++ b/api/app/Observers/ArtistEngagementObserver.php @@ -15,10 +15,10 @@ use App\Models\Scopes\OrganisationScope; * (RFC v0.2 D10) and asserts artist.organisation_id === event.organisation_id. * * On `deleted` (soft delete): cascade soft-delete to child Performance - * rows; hard-delete child AdvanceSection rows (RFC §5.4 — sections have - * no soft delete). Child AdvanceSubmission rows are immutable audit - * records and remain attached to their (now-deleted) section reference - * by FK — this is the audit-retention compliance path. + * rows; hard-delete child AdvanceSection rows (per RFC §5.4 — sections + * have no soft delete). AdvanceSubmission rows remain in the table with + * `advance_section_id = NULL` (FK is `nullOnDelete`) per RFC §5.4 + * audit-immutability — orphaned but preserved for retention compliance. */ final class ArtistEngagementObserver { diff --git a/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php b/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php index 1bf9f4a9..3226ed19 100644 --- a/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php +++ b/api/database/migrations/2026_05_08_100009_create_advance_submissions_table.php @@ -12,7 +12,7 @@ return new class extends Migration { Schema::create('advance_submissions', function (Blueprint $table) { $table->ulid('id')->primary(); - $table->foreignUlid('advance_section_id')->constrained()->cascadeOnDelete(); + $table->foreignUlid('advance_section_id')->nullable()->constrained()->nullOnDelete(); $table->string('submitted_by_name'); $table->string('submitted_by_email'); $table->timestamp('submitted_at'); diff --git a/api/database/schema/mysql-schema.sql b/api/database/schema/mysql-schema.sql index fcc343e0..6a6d947a 100644 --- a/api/database/schema/mysql-schema.sql +++ b/api/database/schema/mysql-schema.sql @@ -55,7 +55,7 @@ DROP TABLE IF EXISTS `advance_submissions`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `advance_submissions` ( `id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, - `advance_section_id` char(26) COLLATE utf8mb4_unicode_ci NOT NULL, + `advance_section_id` char(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `submitted_by_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `submitted_by_email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `submitted_at` timestamp NOT NULL, @@ -68,7 +68,7 @@ CREATE TABLE `advance_submissions` ( PRIMARY KEY (`id`), KEY `advance_submissions_reviewed_by_foreign` (`reviewed_by`), KEY `advance_submissions_advance_section_id_status_index` (`advance_section_id`,`status`), - CONSTRAINT `advance_submissions_advance_section_id_foreign` FOREIGN KEY (`advance_section_id`) REFERENCES `advance_sections` (`id`) ON DELETE CASCADE, + CONSTRAINT `advance_submissions_advance_section_id_foreign` FOREIGN KEY (`advance_section_id`) REFERENCES `advance_sections` (`id`) ON DELETE SET NULL, CONSTRAINT `advance_submissions_reviewed_by_foreign` FOREIGN KEY (`reviewed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; diff --git a/api/tests/Feature/Artist/ArtistEngagementObserverTest.php b/api/tests/Feature/Artist/ArtistEngagementObserverTest.php index b93a9749..3b079ff6 100644 --- a/api/tests/Feature/Artist/ArtistEngagementObserverTest.php +++ b/api/tests/Feature/Artist/ArtistEngagementObserverTest.php @@ -81,7 +81,7 @@ final class ArtistEngagementObserverTest extends TestCase 'type' => 'production', ]); - AdvanceSubmission::create([ + $submission = AdvanceSubmission::create([ 'advance_section_id' => $section->id, 'submitted_by_name' => 'TM', 'submitted_by_email' => 'tm@example.test', @@ -90,6 +90,8 @@ final class ArtistEngagementObserverTest extends TestCase 'data' => [], ]); + $submissionCountBefore = AdvanceSubmission::withoutGlobalScope(OrganisationScope::class)->count(); + $eng->delete(); // Performance is soft-deleted (trashed, not removed). @@ -99,11 +101,15 @@ final class ArtistEngagementObserverTest extends TestCase // AdvanceSection is hard-deleted. $this->assertNull(AdvanceSection::withoutGlobalScope(OrganisationScope::class)->find($section->id)); - // Note: `advance_submissions.advance_section_id` currently uses - // `cascadeOnDelete()`, so submissions are removed with their section. - // RFC v0.2 §5.4 calls submissions "audit-immutable" — interpreted - // here as "no application code mutates them post-creation". A - // future migration may switch the FK to nullOnDelete to preserve - // rows past section hard-delete; out of Session 1 scope. + // AdvanceSubmission survives as audit-orphan per RFC §5.4: row + // preserved, advance_section_id nulled by FK ON DELETE SET NULL. + $this->assertSame( + $submissionCountBefore, + AdvanceSubmission::withoutGlobalScope(OrganisationScope::class)->count(), + 'advance_submissions must persist for retention compliance' + ); + $orphan = AdvanceSubmission::withoutGlobalScope(OrganisationScope::class)->find($submission->id); + $this->assertNotNull($orphan); + $this->assertNull($orphan->advance_section_id); } } -- 2.39.5 From 7eec9d148fab802feb6c6592420714b67765c2cd Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 19:43:19 +0200 Subject: [PATCH 14/14] =?UTF-8?q?docs(backlog):=20record=20portal=5Ftoken?= =?UTF-8?q?=20schema=20deviation=20from=20RFC=20v0.2=20=C2=A75.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dev-docs/BACKLOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 88caa278..00eea35d 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -704,6 +704,27 @@ voor implementatie van Sessions 2–6. --- +### RFC-TIMETABLE-V0.2-PORTAL-TOKEN-SCHEMA-AMEND — `portal_token` is varchar(64), niet ULID + +**Aanleiding:** RFC v0.2 §5.3 specificeert `artist_engagements.portal_token` +als `ULID unique nullable`. Session 1 implementatie heeft de kolom verbreed +naar `varchar(64)` omdat `PortalTokenController` `hash('sha256', $plainToken)` +opslaat (64-char hex digest); `char(26)` zou stilzwijgend truncaten onder +MySQL strict mode. De implementatie is correct — schema reality is de bron +van waarheid — maar de RFC-annotatie is stale. + +**Wat:** Bij de eerstvolgende RFC amendement-cyclus, hetzij een v0.3 +uitbrengen met §5.3 spec gecorrigeerd, hetzij een §5.3 footnote toevoegen +aan v0.2. Approved RFCs worden niet ad-hoc gepatched; dit ticket vangt +de divergentie totdat een legitieme amendement langskomt. + +**Reference:** Session 1 commits `eb6d396` (column widening) en `64878f2` +(controller wired through `artist_engagements.portal_token`). + +**Prioriteit:** Laag — pure doc-spec alignment; code is correct. + +--- + ### TECH-01 — Bestaande tests bijwerken na festival/event refactor **Aanleiding:** Na toevoegen parent_event_id worden bestaande tests -- 2.39.5