feat(timetable): EngagementPortalController + /p/artist/{token}/* routes

Three backend endpoints under public throttle:30,1:
  GET  /p/artist/{token}                       — engagement summary + sections
  GET  /p/artist/{token}/sections/{section}    — form schema + draft values
  POST /p/artist/{token}/sections/{section}    — section submit

Token resolution via ArtistResolver::fromPortalToken (Step 2). The
master Artist becomes the FormSubmission subject; engagement.event_id
populates form_submissions.event_id per WS-4 denormalisation. Token
mismatches map to 404 (InvalidPortalTokenException), soft-deleted
master artists to 410 Gone (ArtistDeletedException).

Section submit reuses the existing FormBindingApplicator pipeline
(RFC-WS-6 v1.3.1) by dispatching FormSubmissionSectionSubmitted —
no parallel apply path. Drafts are idempotent on
'artist_advance:{engagement_id}', so repeated POSTs find the same
submission. AdvanceSection (engagement-scoped) ↔ FormSchemaSection
bridge: case-sensitive name match against the org's artist_advance
schema; the default seeder names them in lockstep.

Frontend in Session 5 — backend complete here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 22:22:02 +02:00
parent 895a1690e7
commit eba162f255
4 changed files with 385 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Portal;
use Illuminate\Foundation\Http\FormRequest;
/**
* Validates the body of POST /p/artist/{token}/sections/{section}.
*
* Body shape: { values: { "<field-slug>": <value>, ... } }
*
* Per-field type validation runs inside FormValueService against the
* form_field_validation_rules rows; this request only enforces the
* envelope shape so we can reject malformed requests early.
*/
final class SubmitEngagementSectionRequest extends FormRequest
{
public function authorize(): bool
{
// Auth lives in the controller (via ArtistResolver token check).
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'values' => ['required', 'array'],
'values.*' => ['nullable'],
];
}
}