From 05e44a39ae2812b8409f69121bf0d3ffec067c54 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 20:45:46 +0200 Subject: [PATCH] feat(timetable): add 5 artist-domain policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArtistPolicy, ArtistEngagementPolicy, StagePolicy, PerformancePolicy, GenrePolicy. Role-based authorization mirroring PersonPolicy/ShiftPolicy pattern: super_admin bypass, org-membership check via wherePivotIn, event_manager fallback for event-level operations. Each policy carries a class-level docblock mapping the RFC §9 permission strings (events.view_program, events.manage_program, organisations.manage_artists, organisations.manage_settings) to the roles authorised, deferring permission-based authorisation to AUTH-PERMISSIONS-MIGRATION. ArtistPolicy.delete additionally guards on no-active-engagements (D27): blocks soft-delete while any engagement is not Cancelled, Rejected, or Declined. PerformancePolicy.move and StagePolicy.reorder reuse canManageProgram so the move endpoint and stage-reorder share the manage_program permission semantics. Auto-discovered by Laravel 11 (policies live at App\Policies\* matching top-level App\Models\* — no explicit Gate::policy registration needed). Co-Authored-By: Claude Opus 4.7 (1M context) --- api/app/Policies/ArtistEngagementPolicy.php | 101 ++++++++++++++++++ api/app/Policies/ArtistPolicy.php | 82 +++++++++++++++ api/app/Policies/GenrePolicy.php | 59 +++++++++++ api/app/Policies/PerformancePolicy.php | 110 ++++++++++++++++++++ api/app/Policies/StagePolicy.php | 106 +++++++++++++++++++ 5 files changed, 458 insertions(+) create mode 100644 api/app/Policies/ArtistEngagementPolicy.php create mode 100644 api/app/Policies/ArtistPolicy.php create mode 100644 api/app/Policies/GenrePolicy.php create mode 100644 api/app/Policies/PerformancePolicy.php create mode 100644 api/app/Policies/StagePolicy.php diff --git a/api/app/Policies/ArtistEngagementPolicy.php b/api/app/Policies/ArtistEngagementPolicy.php new file mode 100644 index 00000000..364feb29 --- /dev/null +++ b/api/app/Policies/ArtistEngagementPolicy.php @@ -0,0 +1,101 @@ +canViewProgram($user, $event); + } + + public function view(User $user, ArtistEngagement $engagement, Event $event): bool + { + if ($engagement->event_id !== $event->id) { + return false; + } + + return $this->canViewProgram($user, $event); + } + + public function create(User $user, Event $event): bool + { + return $this->canManageProgram($user, $event); + } + + public function update(User $user, ArtistEngagement $engagement, Event $event): bool + { + if ($engagement->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + public function delete(User $user, ArtistEngagement $engagement, Event $event): bool + { + if ($engagement->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + private function canViewProgram(User $user, Event $event): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + $isOrgMember = $event->organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager', 'production_assistant']) + ->exists(); + + if ($isOrgMember) { + return true; + } + + return $event->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'event_manager') + ->exists(); + } + + private function canManageProgram(User $user, Event $event): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + $isOrgManager = $event->organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager']) + ->exists(); + + if ($isOrgManager) { + return true; + } + + return $event->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'event_manager') + ->exists(); + } +} diff --git a/api/app/Policies/ArtistPolicy.php b/api/app/Policies/ArtistPolicy.php new file mode 100644 index 00000000..06205898 --- /dev/null +++ b/api/app/Policies/ArtistPolicy.php @@ -0,0 +1,82 @@ +hasRole('super_admin') + || $organisation->users()->where('user_id', $user->id)->exists(); + } + + public function view(User $user, Artist $artist): bool + { + return $user->hasRole('super_admin') + || $artist->organisation->users()->where('user_id', $user->id)->exists(); + } + + public function create(User $user, Organisation $organisation): bool + { + return $this->canManageArtists($user, $organisation); + } + + public function update(User $user, Artist $artist): bool + { + return $this->canManageArtists($user, $artist->organisation); + } + + /** + * Soft-delete is blocked while the artist still has any non-terminal + * engagement (D27 — soft-delete preserves historical engagements but + * the master must not vanish underneath an active booking). + */ + public function delete(User $user, Artist $artist): bool + { + if (! $this->canManageArtists($user, $artist->organisation)) { + return false; + } + + $hasActive = $artist->engagements() + ->whereNotIn('booking_status', [ + ArtistEngagementStatus::Cancelled->value, + ArtistEngagementStatus::Rejected->value, + ArtistEngagementStatus::Declined->value, + ]) + ->exists(); + + return ! $hasActive; + } + + public function restore(User $user, Artist $artist): bool + { + return $this->canManageArtists($user, $artist->organisation); + } + + private function canManageArtists(User $user, Organisation $organisation): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + return $organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager']) + ->exists(); + } +} diff --git a/api/app/Policies/GenrePolicy.php b/api/app/Policies/GenrePolicy.php new file mode 100644 index 00000000..99145b47 --- /dev/null +++ b/api/app/Policies/GenrePolicy.php @@ -0,0 +1,59 @@ +hasRole('super_admin') + || $organisation->users()->where('user_id', $user->id)->exists(); + } + + public function view(User $user, Genre $genre): bool + { + return $user->hasRole('super_admin') + || $genre->organisation->users()->where('user_id', $user->id)->exists(); + } + + public function create(User $user, Organisation $organisation): bool + { + return $this->canManageSettings($user, $organisation); + } + + public function update(User $user, Genre $genre): bool + { + return $this->canManageSettings($user, $genre->organisation); + } + + public function delete(User $user, Genre $genre): bool + { + return $this->canManageSettings($user, $genre->organisation); + } + + private function canManageSettings(User $user, Organisation $organisation): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + return $organisation->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'org_admin') + ->exists(); + } +} diff --git a/api/app/Policies/PerformancePolicy.php b/api/app/Policies/PerformancePolicy.php new file mode 100644 index 00000000..df7481a8 --- /dev/null +++ b/api/app/Policies/PerformancePolicy.php @@ -0,0 +1,110 @@ +canViewProgram($user, $event); + } + + public function view(User $user, Performance $performance, Event $event): bool + { + if ($performance->event_id !== $event->id) { + return false; + } + + return $this->canViewProgram($user, $event); + } + + public function create(User $user, Event $event): bool + { + return $this->canManageProgram($user, $event); + } + + public function update(User $user, Performance $performance, Event $event): bool + { + if ($performance->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + public function delete(User $user, Performance $performance, Event $event): bool + { + if ($performance->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + public function move(User $user, Performance $performance, Event $event): bool + { + if ($performance->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + private function canViewProgram(User $user, Event $event): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + $isOrgMember = $event->organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager', 'production_assistant']) + ->exists(); + + if ($isOrgMember) { + return true; + } + + return $event->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'event_manager') + ->exists(); + } + + private function canManageProgram(User $user, Event $event): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + $isOrgManager = $event->organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager']) + ->exists(); + + if ($isOrgManager) { + return true; + } + + return $event->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'event_manager') + ->exists(); + } +} diff --git a/api/app/Policies/StagePolicy.php b/api/app/Policies/StagePolicy.php new file mode 100644 index 00000000..74b5f9c4 --- /dev/null +++ b/api/app/Policies/StagePolicy.php @@ -0,0 +1,106 @@ +canViewProgram($user, $event); + } + + public function view(User $user, Stage $stage, Event $event): bool + { + if ($stage->event_id !== $event->id) { + return false; + } + + return $this->canViewProgram($user, $event); + } + + public function create(User $user, Event $event): bool + { + return $this->canManageProgram($user, $event); + } + + public function update(User $user, Stage $stage, Event $event): bool + { + if ($stage->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + public function delete(User $user, Stage $stage, Event $event): bool + { + if ($stage->event_id !== $event->id) { + return false; + } + + return $this->canManageProgram($user, $event); + } + + public function reorder(User $user, Event $event): bool + { + return $this->canManageProgram($user, $event); + } + + private function canViewProgram(User $user, Event $event): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + $isOrgMember = $event->organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager', 'production_assistant']) + ->exists(); + + if ($isOrgMember) { + return true; + } + + return $event->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'event_manager') + ->exists(); + } + + private function canManageProgram(User $user, Event $event): bool + { + if ($user->hasRole('super_admin')) { + return true; + } + + $isOrgManager = $event->organisation->users() + ->where('user_id', $user->id) + ->wherePivotIn('role', ['org_admin', 'program_manager']) + ->exists(); + + if ($isOrgManager) { + return true; + } + + return $event->users() + ->where('user_id', $user->id) + ->wherePivot('role', 'event_manager') + ->exists(); + } +}