Files
crewli/api/app/Rules/Artist/WithinEventBounds.php
bert.hausmans 378b6fe970 feat(timetable): four custom validation rules for artist domain
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) <noreply@anthropic.com>
2026-05-08 20:50:12 +02:00

52 lines
1.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Rules\Artist;
use App\Models\Event;
use Carbon\CarbonImmutable;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Validates that a candidate datetime sits inside the event's
* [start_at, end_at] window. Works for flat events (single window)
* and for sub-events (sub-event window); festival-level events are
* usually not the target — the FormRequest passes the *sub-event* id
* for performances and the *containing* event for the stage's day.
*
* Rule passes silently when value is null or when the event cannot
* be found (the FormRequest owns the `required`/`exists` decision).
*/
final class WithinEventBounds implements ValidationRule
{
public function __construct(
private readonly ?string $eventId,
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($value === null || $this->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'),
));
}
}
}