feat(form-builder): extend public form backend for S3a PR 2

- Seed AVAILABILITY_PICKER and SECTION_PRIORITY demo fields in the
  event_registration showcase, and augment seedEchtFeesten with a
  parent-level VOLUNTEER time slot pair + a standard registration-
  visible section whose name duplicates a child section so the
  PublicFormController dedup path is exercised end-to-end.
- Validate SECTION_PRIORITY value shape in FormValueService: arrays of
  { section_id, priority } with unique section_ids + priorities in 1..5,
  max 5 entries, and section_ids scoped to the schema's event tree
  (parent + children). Error envelope is the standard VALIDATION_FAILED
  FieldValidationException shape so the portal renders errors next to
  the field.
- Enrich admin-facing FormSubmissionResource with a nested identity_match
  block mirroring the PublicFormSubmissionResource contract (status only;
  leaves room for future matched_user_id / confidence).
- Lock in the FORM-05 stub contract with 6 tests against the existing
  TriggerPersonIdentityMatchOnFormSubmit listener (no new listener was
  needed — the current one already writes 'pending' for public
  event_registration submissions per ARCH §31.1).
- 24 new backend assertions across seeder, shape validation, listener
  state matrix, and resource serialisation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 18:54:58 +02:00
parent d274284fd4
commit 1a87871e94
8 changed files with 957 additions and 0 deletions

View File

@@ -285,6 +285,21 @@ class DevSeeder extends Seeder
'show_in_registration' => false,
]);
// Parent-level standard section that mirrors a child name — exercises
// PublicFormController::sections() dedup across parent+child.
FestivalSection::create([
'event_id' => $festival->id,
'name' => 'Hoofdpodium Bar',
'type' => 'standard',
'category' => 'Bar',
'icon' => 'tabler-beer',
'sort_order' => 5,
'responder_self_checkin' => true,
'crew_auto_accepts' => true,
'show_in_registration' => true,
'registration_description' => 'Overkoepelende barinzet — je draait mee op het hoofdpodium over alle dagen',
]);
// ── Sub-event sections (5 per sub-event) ──
$sectionDefs = [
@@ -324,6 +339,11 @@ class DevSeeder extends Seeder
$fSlots['nacht_za'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Nachtsecurity za→zo', 'person_type' => 'CREW', 'date' => '2026-07-11', 'start_time' => '23:00', 'end_time' => '07:00', 'duration_hours' => 8.00]);
$fSlots['afbraak'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Afbraak', 'person_type' => 'CREW', 'date' => '2026-07-13', 'start_time' => '08:00', 'end_time' => '18:00', 'duration_hours' => 10.00]);
// Parent-level VOLUNTEER slots — ensures PublicFormController::timeSlots
// surfaces both parent and child events in the merged response.
$fSlots['vol_opbouw_vr'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Vrijwilligers opbouw vrijdag', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-10', 'start_time' => '09:00', 'end_time' => '12:00', 'duration_hours' => 3.00]);
$fSlots['vol_afbraak_zo'] = TimeSlot::create(['event_id' => $festival->id, 'name' => 'Vrijwilligers afbraak zondag', 'person_type' => 'VOLUNTEER', 'date' => '2026-07-13', 'start_time' => '10:00', 'end_time' => '16:00', 'duration_hours' => 6.00]);
// ── Sub-event time slots (program-specific) ──
$ts = [];

View File

@@ -293,6 +293,29 @@ final class FormBuilderDevSeeder
'display_width' => 'full',
'value_storage_hint' => FormValueStorageHint::JSON,
],
[
'type' => FormFieldType::AVAILABILITY_PICKER,
'slug' => 'beschikbaarheid',
'label' => 'Wanneer ben je beschikbaar?',
'help_text' => 'Vink alle dagdelen aan waarop je kunt werken.',
'is_required' => true,
'is_filterable' => false,
'display_width' => 'full',
'value_storage_hint' => FormValueStorageHint::JSON,
],
[
'type' => FormFieldType::SECTION_PRIORITY,
'slug' => 'sectie_voorkeur',
'label' => 'Bij welke sectie wil je het liefst werken?',
'help_text' => 'Sleep je voorkeuren in volgorde. Nummer 1 is je eerste keuze.',
// UI soft cap; the hard cap of 5 lives in
// FormValueService shape validation.
'validation_rules' => ['max_priorities' => 3],
'is_required' => false,
'is_filterable' => false,
'display_width' => 'full',
'value_storage_hint' => FormValueStorageHint::JSON,
],
[
'type' => FormFieldType::TEXTAREA,
'slug' => 'opmerkingen',
@@ -340,6 +363,7 @@ final class FormBuilderDevSeeder
'field_type' => $def['type']->value,
'slug' => $def['slug'],
'label' => $def['label'],
'help_text' => $def['help_text'] ?? null,
'options' => $def['options'] ?? null,
'validation_rules' => $def['validation_rules'] ?? null,
'is_required' => $def['is_required'] ?? false,