From 378b6fe970b29bcb5791f5bb17e32c1c6bad4cb0 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 20:50:12 +0200 Subject: [PATCH] feat(timetable): four custom validation rules for artist domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StageActiveOnEvent — checks the candidate stage_id is linked to the given event_id via stage_days. Covers performance create/update (perf.event_id ↔ stage) and the timetable move endpoint (target_stage_id ↔ resolved target event). WithinEventBounds — checks a candidate datetime is inside the event's [start_at, end_at] window. Used for performance start/end dates and move-target dates against the relevant sub-event for festivals. OptionExpiresInFuture — conditional rule fired only when booking_status === 'option'. Asserts option_expires_at is set and in the future. Implementation of RFC §10.1 transition gate at the request layer (the service layer enforces the same invariant). ContractRequiresFee — conditional rule fired only when booking_status === 'contracted'. Asserts fee_amount is set and > 0. Same dual-layer enforcement as OptionExpiresInFuture. All four pass silently when the validated field is null or the context is irrelevant — the FormRequest still owns the surrounding required/nullable/exists rules. Co-Authored-By: Claude Opus 4.7 (1M context) --- api/app/Rules/Artist/ContractRequiresFee.php | 40 +++++++++++++++ .../Rules/Artist/OptionExpiresInFuture.php | 42 +++++++++++++++ api/app/Rules/Artist/StageActiveOnEvent.php | 43 ++++++++++++++++ api/app/Rules/Artist/WithinEventBounds.php | 51 +++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 api/app/Rules/Artist/ContractRequiresFee.php create mode 100644 api/app/Rules/Artist/OptionExpiresInFuture.php create mode 100644 api/app/Rules/Artist/StageActiveOnEvent.php create mode 100644 api/app/Rules/Artist/WithinEventBounds.php diff --git a/api/app/Rules/Artist/ContractRequiresFee.php b/api/app/Rules/Artist/ContractRequiresFee.php new file mode 100644 index 00000000..63021c68 --- /dev/null +++ b/api/app/Rules/Artist/ContractRequiresFee.php @@ -0,0 +1,40 @@ +bookingStatus !== ArtistEngagementStatus::Contracted->value) { + return; + } + + if ($value === null || $value === '') { + $fail('Bij status "Gecontracteerd" is een fee verplicht.'); + + return; + } + + if ((float) $value <= 0) { + $fail('De fee moet groter dan 0 zijn.'); + } + } +} diff --git a/api/app/Rules/Artist/OptionExpiresInFuture.php b/api/app/Rules/Artist/OptionExpiresInFuture.php new file mode 100644 index 00000000..cfc67eb6 --- /dev/null +++ b/api/app/Rules/Artist/OptionExpiresInFuture.php @@ -0,0 +1,42 @@ +bookingStatus !== ArtistEngagementStatus::Option->value) { + return; + } + + if ($value === null || $value === '') { + $fail('Bij status "Optie" is een vervaldatum verplicht.'); + + return; + } + + $candidate = CarbonImmutable::parse((string) $value); + if ($candidate->isPast()) { + $fail('De optie-vervaldatum moet in de toekomst liggen.'); + } + } +} diff --git a/api/app/Rules/Artist/StageActiveOnEvent.php b/api/app/Rules/Artist/StageActiveOnEvent.php new file mode 100644 index 00000000..5dfde581 --- /dev/null +++ b/api/app/Rules/Artist/StageActiveOnEvent.php @@ -0,0 +1,43 @@ +eventId === null) { + return; + } + + $exists = StageDay::query() + ->where('stage_id', (string) $value) + ->where('event_id', $this->eventId) + ->exists(); + + if (! $exists) { + $fail('De gekozen stage is niet actief op deze (sub-)event.'); + } + } +} diff --git a/api/app/Rules/Artist/WithinEventBounds.php b/api/app/Rules/Artist/WithinEventBounds.php new file mode 100644 index 00000000..255d8118 --- /dev/null +++ b/api/app/Rules/Artist/WithinEventBounds.php @@ -0,0 +1,51 @@ +eventId === null) { + return; + } + + $event = Event::withoutGlobalScopes()->find($this->eventId); + if ($event === null) { + return; + } + + $candidate = CarbonImmutable::parse((string) $value); + $start = CarbonImmutable::instance($event->start_at); + $end = CarbonImmutable::instance($event->end_at); + + if ($candidate->lt($start) || $candidate->gt($end)) { + $fail(sprintf( + 'De datum moet binnen het evenement (%s — %s) vallen.', + $start->format('Y-m-d H:i'), + $end->format('Y-m-d H:i'), + )); + } + } +}