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

@@ -97,6 +97,15 @@ Route::post('verify-email-change', [EmailChangeController::class, 'verify']);
Route::post('public/check-email', CheckEmailController::class)->middleware('throttle:10,1');
Route::post('portal/token-auth', [PortalTokenController::class, 'auth'])->middleware('throttle:10,1');
// RFC-TIMETABLE v0.2 D15 — artist advance portal (public, token-scoped).
// Token is the plaintext portal token; resolution via ArtistResolver
// inside the controller. Throttled like the other public portal routes.
Route::middleware('throttle:30,1')->prefix('p/artist/{token}')->group(function (): void {
Route::get('/', [\App\Http\Controllers\Api\V1\Portal\EngagementPortalController::class, 'show']);
Route::get('sections/{section}', [\App\Http\Controllers\Api\V1\Portal\EngagementPortalController::class, 'showSection']);
Route::post('sections/{section}', [\App\Http\Controllers\Api\V1\Portal\EngagementPortalController::class, 'submitSection']);
});
// Public Form Builder routes (no auth — token-based, rate-limited per ARCH §10).
// S2c D4: draft/save/submit split into three separate endpoints.
Route::middleware('throttle:30,1')->group(function (): void {