- 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>
573 lines
25 KiB
PHP
573 lines
25 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Enums\FormBuilder\FormFieldType;
|
|
use App\Enums\FormBuilder\FormPurpose;
|
|
use App\Enums\FormBuilder\FormSchemaSnapshotMode;
|
|
use App\Enums\FormBuilder\FormSubmissionMode;
|
|
use App\Enums\FormBuilder\FormSubmissionStatus;
|
|
use App\Enums\FormBuilder\FormValueStorageHint;
|
|
use App\Models\Event;
|
|
use App\Models\FormBuilder\FormField;
|
|
use App\Models\FormBuilder\FormSchema;
|
|
use App\Models\FormBuilder\FormSubmission;
|
|
use App\Models\FormBuilder\FormTemplate;
|
|
use App\Models\FormBuilder\FormValue;
|
|
use App\Models\Organisation;
|
|
use App\Models\Person;
|
|
use App\Models\PersonTag;
|
|
use App\Models\UserOrganisationTag;
|
|
use App\Services\FormBuilder\FormSubmissionService;
|
|
use App\Services\FormBuilder\FormValueService;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Seeds the form-builder layer for DevSeeder: 16 system form_templates per
|
|
* org, one FormSchema per event, the canonical 16-field registration set,
|
|
* and a realistic batch of FormSubmissions for approved/applied/no_show
|
|
* persons (same rule as the forms:migrate-legacy-data command).
|
|
*
|
|
* Callers MUST wrap this in ActivityLog::suppressed() — every field create
|
|
* would otherwise fire a logFieldChange("created") entry.
|
|
*/
|
|
final class FormBuilderDevSeeder
|
|
{
|
|
/**
|
|
* 16 canonical registration fields mirroring the legacy system templates.
|
|
* Used by both seedSystemTemplates() and seedEventSchema() so the shape
|
|
* stays aligned.
|
|
*
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function canonicalFields(): array
|
|
{
|
|
return [
|
|
['type' => FormFieldType::HEADING, 'slug' => 'persoonlijke-voorkeuren', 'label' => 'Persoonlijke voorkeuren', 'help_text' => 'Vertel ons wat we over jou moeten weten', 'display_width' => 'full'],
|
|
['type' => FormFieldType::SELECT, 'slug' => 'shirtmaat', 'label' => 'Shirtmaat', 'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'], 'is_filterable' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::MULTISELECT, 'slug' => 'dieetwensen', 'label' => 'Dieetwensen', 'options' => ['Vegetarisch', 'Veganistisch', 'Halal', 'Glutenvrij', 'Lactosevrij', "Geen pinda's", 'Geen noten'], 'is_filterable' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::HEADING, 'slug' => 'vergoeding', 'label' => 'Vergoeding', 'help_text' => 'Kies hoe je wilt worden bedankt voor je inzet', 'display_width' => 'full'],
|
|
['type' => FormFieldType::RADIO, 'slug' => 'vergoedingstype', 'label' => 'Vergoedingstype', 'options' => [
|
|
['label' => 'Pro Deo', 'description' => 'Je werkt als vrijwilliger zonder financiële vergoeding'],
|
|
['label' => 'Entreeticket', 'description' => 'Je ontvangt een gratis festivalticket als dank voor je inzet'],
|
|
['label' => 'Vrijwilligersvergoeding', 'description' => 'Je ontvangt een vergoeding conform de vrijwilligersregeling'],
|
|
], 'is_required' => true, 'display_width' => 'full'],
|
|
['type' => FormFieldType::HEADING, 'slug' => 'noodcontact', 'label' => 'Noodcontact', 'help_text' => 'Wie kunnen we bereiken bij calamiteiten?', 'display_width' => 'full'],
|
|
['type' => FormFieldType::TEXT, 'slug' => 'noodcontact-naam', 'label' => 'Noodcontact naam', 'is_pii' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::TEXT, 'slug' => 'noodcontact-telefoon', 'label' => 'Noodcontact telefoon', 'is_pii' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::HEADING, 'slug' => 'ervaring-vaardigheden', 'label' => 'Ervaring & vaardigheden', 'help_text' => "Welke diploma's en skills heb je?", 'display_width' => 'full'],
|
|
['type' => FormFieldType::BOOLEAN, 'slug' => 'ehbo-bhv-diploma', 'label' => 'EHBO / BHV diploma', 'is_filterable' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::BOOLEAN, 'slug' => 'rijbewijs', 'label' => 'Rijbewijs', 'is_filterable' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::BOOLEAN, 'slug' => 'eerder-vrijwilliger-geweest', 'label' => 'Eerder vrijwilliger geweest', 'is_filterable' => true, 'display_width' => 'half'],
|
|
['type' => FormFieldType::TAG_PICKER, 'slug' => 'certificaten-vaardigheden', 'label' => 'Certificaten & vaardigheden', 'is_filterable' => true, 'display_width' => 'full'],
|
|
['type' => FormFieldType::HEADING, 'slug' => 'aanvullende-informatie', 'label' => 'Aanvullende informatie', 'display_width' => 'full'],
|
|
['type' => FormFieldType::BOOLEAN, 'slug' => 'toestemming-gegevensverwerking', 'label' => 'Toestemming gegevensverwerking', 'help_text' => 'Ik geef toestemming voor de verwerking van mijn persoonsgegevens ten behoeve van de organisatie van dit evenement, conform de Algemene Verordening Gegevensbescherming (AVG).', 'is_required' => true, 'display_width' => 'full'],
|
|
['type' => FormFieldType::TEXTAREA, 'slug' => 'opmerkingen', 'label' => 'Opmerkingen', 'display_width' => 'full'],
|
|
];
|
|
}
|
|
|
|
public static function seedSystemTemplates(Organisation $org): int
|
|
{
|
|
$count = 0;
|
|
foreach (self::canonicalFields() as $sortOrder => $field) {
|
|
$snapshot = [
|
|
'schema_version' => 1,
|
|
'snapshot_created_at' => now()->toIso8601String(),
|
|
'schema' => [
|
|
'name' => $field['label'].' (template)',
|
|
'slug' => $field['slug'],
|
|
'purpose' => FormPurpose::EVENT_REGISTRATION->value,
|
|
'description' => null,
|
|
'locale' => 'nl',
|
|
'freeze_on_submit' => false,
|
|
'section_level_submit' => false,
|
|
'settings' => [],
|
|
],
|
|
'sections' => [],
|
|
'fields' => [[
|
|
'id' => (string) Str::ulid(),
|
|
'slug' => $field['slug'],
|
|
'field_type' => $field['type']->value,
|
|
'label' => $field['label'],
|
|
'help_text' => $field['help_text'] ?? null,
|
|
'section_slug' => null,
|
|
'options' => $field['options'] ?? null,
|
|
'validation_rules' => null,
|
|
'is_required' => $field['is_required'] ?? false,
|
|
'is_filterable' => $field['is_filterable'] ?? false,
|
|
'is_pii' => $field['is_pii'] ?? false,
|
|
'binding' => null,
|
|
'conditional_logic' => null,
|
|
'translations' => null,
|
|
'value_storage_hint' => $field['type']->recommendedValueStorageHint()->value,
|
|
'sort_order' => $sortOrder + 1,
|
|
]],
|
|
];
|
|
|
|
FormTemplate::create([
|
|
'organisation_id' => $org->id,
|
|
'name' => $field['label'],
|
|
'slug' => $field['slug'],
|
|
'purpose' => FormPurpose::EVENT_REGISTRATION,
|
|
'description' => null,
|
|
'schema_snapshot' => $snapshot,
|
|
'is_active' => true,
|
|
])->forceFill(['is_system' => true])->save();
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Create one FormSchema (event_registration) for this event with the
|
|
* canonical 16-field set.
|
|
*/
|
|
public static function seedEventSchema(Event $event): FormSchema
|
|
{
|
|
$schema = FormSchema::create([
|
|
'organisation_id' => $event->organisation_id,
|
|
'owner_type' => 'event',
|
|
'owner_id' => $event->id,
|
|
'name' => $event->name.' — registratie',
|
|
'slug' => Str::slug($event->slug.'-registratie'),
|
|
'purpose' => FormPurpose::EVENT_REGISTRATION,
|
|
'description' => "Registratieformulier voor {$event->name}.",
|
|
'is_published' => in_array(
|
|
$event->status,
|
|
['registration_open', 'buildup', 'showday', 'teardown', 'closed'],
|
|
true
|
|
),
|
|
'submission_mode' => FormSubmissionMode::DRAFT_SINGLE,
|
|
'locale' => 'nl',
|
|
'snapshot_mode' => FormSchemaSnapshotMode::NEVER,
|
|
'freeze_on_submit' => false,
|
|
'section_level_submit' => false,
|
|
'auto_save_enabled' => false,
|
|
]);
|
|
|
|
foreach (self::canonicalFields() as $sortOrder => $field) {
|
|
FormField::create([
|
|
'form_schema_id' => $schema->id,
|
|
'field_type' => $field['type']->value,
|
|
'slug' => $field['slug'],
|
|
'label' => $field['label'],
|
|
'help_text' => $field['help_text'] ?? null,
|
|
'options' => $field['options'] ?? null,
|
|
'is_required' => $field['is_required'] ?? false,
|
|
'is_filterable' => $field['is_filterable'] ?? false,
|
|
'is_portal_visible' => true,
|
|
'is_admin_only' => false,
|
|
'is_pii' => $field['is_pii'] ?? false,
|
|
'display_width' => $field['display_width'] ?? 'full',
|
|
'value_storage_hint' => $field['type']->recommendedValueStorageHint(),
|
|
'sort_order' => $sortOrder + 1,
|
|
]);
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* For each person with status ∈ applied/approved/no_show on this event,
|
|
* create one FormSubmission with a realistic handful of FormValues.
|
|
* Status follows the same rule as MigrateLegacyFormsData.
|
|
*/
|
|
public static function seedSubmissionsForEvent(Event $event, FormSchema $schema): int
|
|
{
|
|
$fields = $schema->fields()->get()->keyBy('slug');
|
|
$persons = Person::where('event_id', $event->id)
|
|
->whereIn('status', ['applied', 'approved', 'no_show'])
|
|
->get();
|
|
|
|
$count = 0;
|
|
foreach ($persons as $person) {
|
|
$isSubmittedStatus = in_array($person->status, ['applied', 'approved', 'no_show'], true);
|
|
$status = $isSubmittedStatus ? FormSubmissionStatus::SUBMITTED : FormSubmissionStatus::DRAFT;
|
|
|
|
$submission = FormSubmission::create([
|
|
'form_schema_id' => $schema->id,
|
|
'subject_type' => 'person',
|
|
'subject_id' => $person->id,
|
|
'submitted_by_user_id' => $person->user_id,
|
|
'status' => $status,
|
|
'is_test' => false,
|
|
'submitted_in_locale' => 'nl',
|
|
'submitted_at' => $status === FormSubmissionStatus::SUBMITTED ? now()->subDays(rand(1, 30)) : null,
|
|
'schema_version_at_submit' => $status === FormSubmissionStatus::SUBMITTED ? 1 : null,
|
|
]);
|
|
|
|
$seed = crc32((string) $person->id);
|
|
$sizes = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
|
|
self::createValueIfField($fields, 'shirtmaat', ['value' => $sizes[$seed % 6]], $submission);
|
|
self::createValueIfField($fields, 'dieetwensen', ($seed % 3 === 0) ? ['Vegetarisch'] : [], $submission);
|
|
self::createValueIfField($fields, 'rijbewijs', ['value' => ($seed % 2) === 0], $submission);
|
|
self::createValueIfField($fields, 'ehbo-bhv-diploma', ['value' => ($seed % 4) === 0], $submission);
|
|
self::createValueIfField($fields, 'noodcontact-naam', ['value' => 'Partner / Familielid'], $submission);
|
|
self::createValueIfField($fields, 'toestemming-gegevensverwerking', ['value' => true], $submission);
|
|
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @param \Illuminate\Support\Collection<string, FormField> $fields
|
|
* @param mixed $value
|
|
*/
|
|
private static function createValueIfField($fields, string $slug, $value, FormSubmission $submission): void
|
|
{
|
|
$field = $fields->get($slug);
|
|
if ($field === null) {
|
|
return;
|
|
}
|
|
|
|
FormValue::create([
|
|
'form_submission_id' => $submission->id,
|
|
'form_field_id' => $field->id,
|
|
'value' => $value,
|
|
]);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Sprint 0.5 — event_registration showcase (FORM-02 / §31.10 demo)
|
|
//
|
|
// Creates ONE dedicated, public-token-enabled event_registration schema
|
|
// per dev org, with a curated 5-field set, a draft submission, and a
|
|
// fully-submitted submission whose TAG_PICKER values exercise the
|
|
// SyncTagPickerSelectionsOnSubmit listener so user_organisation_tags
|
|
// populate during `migrate:fresh --seed`.
|
|
// =========================================================================
|
|
|
|
/**
|
|
* Showcase field definitions. 5 fields per Sprint 0.5 spec.
|
|
*
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private static function showcaseFieldDefinitions(): array
|
|
{
|
|
return [
|
|
[
|
|
'type' => FormFieldType::HEADING,
|
|
'slug' => 'over-jou',
|
|
'label' => 'Over jou',
|
|
'is_required' => false,
|
|
'is_filterable' => false,
|
|
'display_width' => 'full',
|
|
'value_storage_hint' => FormValueStorageHint::JSON,
|
|
],
|
|
[
|
|
'type' => FormFieldType::SELECT,
|
|
'slug' => 'shirtmaat',
|
|
'label' => 'Shirtmaat',
|
|
'options' => ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
|
|
'is_required' => true,
|
|
'is_filterable' => true,
|
|
'display_width' => 'half',
|
|
'value_storage_hint' => FormValueStorageHint::STRING,
|
|
],
|
|
[
|
|
'type' => FormFieldType::CHECKBOX_LIST,
|
|
'slug' => 'dieetwensen',
|
|
'label' => 'Dieetwensen',
|
|
'options' => ['Vegetarisch', 'Veganistisch', 'Glutenvrij', 'Lactosevrij', 'Halal', 'Kosher'],
|
|
'is_required' => false,
|
|
'is_filterable' => true,
|
|
'display_width' => 'half',
|
|
'value_storage_hint' => FormValueStorageHint::JSON,
|
|
],
|
|
[
|
|
'type' => FormFieldType::TAG_PICKER,
|
|
'slug' => 'vaardigheden',
|
|
'label' => 'Vaardigheden en certificaten',
|
|
// validation_rules.tag_categories = [] means "all active
|
|
// person_tags for this org" — the FormFieldResource picks
|
|
// up every active tag when no category filter is set.
|
|
'validation_rules' => null,
|
|
'is_required' => false,
|
|
'is_filterable' => true,
|
|
'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',
|
|
'label' => 'Opmerkingen',
|
|
'is_required' => false,
|
|
'is_filterable' => false,
|
|
'display_width' => 'full',
|
|
'value_storage_hint' => FormValueStorageHint::STRING,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create the public-token-enabled showcase schema on the given primary
|
|
* event, plus its 5 fields. Returns the schema for follow-up seeding.
|
|
*/
|
|
public static function seedEventRegistrationShowcaseSchema(Organisation $org, Event $event): FormSchema
|
|
{
|
|
$name = 'Vrijwilligersregistratie '.$event->name;
|
|
$slug = self::uniqueSchemaSlug($org, Str::slug($name));
|
|
|
|
/** @var FormSchema $schema */
|
|
$schema = FormSchema::create([
|
|
'organisation_id' => $org->id,
|
|
'owner_type' => 'event',
|
|
'owner_id' => $event->id,
|
|
'name' => $name,
|
|
'slug' => $slug,
|
|
'purpose' => FormPurpose::EVENT_REGISTRATION,
|
|
'description' => "Demo-formulier voor het end-to-end doorlopen van de vrijwilligersregistratie voor {$event->name}.",
|
|
'is_published' => true,
|
|
'submission_mode' => FormSubmissionMode::DRAFT_SINGLE,
|
|
'public_token' => (string) Str::ulid(),
|
|
'locale' => 'nl',
|
|
'snapshot_mode' => FormSchemaSnapshotMode::ON_SUBMIT,
|
|
'freeze_on_submit' => false,
|
|
'section_level_submit' => false,
|
|
'auto_save_enabled' => false,
|
|
'version' => 1,
|
|
]);
|
|
|
|
foreach (self::showcaseFieldDefinitions() as $sortOrder => $def) {
|
|
FormField::create([
|
|
'form_schema_id' => $schema->id,
|
|
'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,
|
|
'is_filterable' => $def['is_filterable'] ?? false,
|
|
'is_portal_visible' => true,
|
|
'is_admin_only' => false,
|
|
'is_pii' => false,
|
|
'display_width' => $def['display_width'] ?? 'full',
|
|
'binding' => null,
|
|
'role_restrictions' => null,
|
|
'value_storage_hint' => $def['value_storage_hint'] ?? FormValueStorageHint::JSON,
|
|
'sort_order' => $sortOrder + 1,
|
|
]);
|
|
}
|
|
|
|
return $schema->refresh();
|
|
}
|
|
|
|
/**
|
|
* Seed one draft + one fully-submitted submission on the showcase
|
|
* schema. The submitted one triggers FormSubmissionSubmitted so the
|
|
* §31.10 listener runs and `user_organisation_tags` picks up
|
|
* `source=self_reported` rows for the submitter's user.
|
|
*
|
|
* Returns summary stats for caller-side logging.
|
|
*
|
|
* @return array{draft_person: ?Person, submitted_person: ?Person, synced_tag_count: int}
|
|
*/
|
|
public static function seedEventRegistrationShowcaseSubmissions(FormSchema $schema, Organisation $org): array
|
|
{
|
|
$event = Event::withoutGlobalScopes()->find($schema->owner_id);
|
|
if ($event === null) {
|
|
return ['draft_person' => null, 'submitted_person' => null, 'synced_tag_count' => 0];
|
|
}
|
|
|
|
// Deterministic pick: first two approved persons with a user_id
|
|
// on this event, ordered by ULID (chronological insertion order).
|
|
$candidates = Person::withoutGlobalScopes()
|
|
->where('event_id', $event->id)
|
|
->whereNotNull('user_id')
|
|
->orderBy('id')
|
|
->take(2)
|
|
->get();
|
|
|
|
$draftPerson = $candidates->first();
|
|
$submittedPerson = $candidates->skip(1)->first();
|
|
|
|
if ($draftPerson !== null) {
|
|
self::buildDraftSubmission($schema, $draftPerson);
|
|
}
|
|
|
|
$syncedTagCount = 0;
|
|
if ($submittedPerson !== null) {
|
|
$syncedTagCount = self::buildSubmittedSubmission($schema, $org, $submittedPerson);
|
|
}
|
|
|
|
return [
|
|
'draft_person' => $draftPerson,
|
|
'submitted_person' => $submittedPerson,
|
|
'synced_tag_count' => $syncedTagCount,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Orchestration helper: schema + submissions + console output, matching
|
|
* the exact format Bert asked for in Sprint 0.5.
|
|
*/
|
|
public static function seedEventRegistrationShowcase(
|
|
Organisation $org,
|
|
Event $event,
|
|
?Command $command = null,
|
|
): FormSchema {
|
|
$schema = self::seedEventRegistrationShowcaseSchema($org, $event);
|
|
|
|
$appUrl = rtrim((string) config('app.url'), '/');
|
|
$publicUrl = "{$appUrl}/api/v1/public/forms/{$schema->public_token}";
|
|
|
|
if ($command !== null) {
|
|
$command->info("[FormBuilderDevSeeder] Seeded event_registration schema for {$org->name}:");
|
|
$command->info(" public URL: {$publicUrl}");
|
|
}
|
|
|
|
$stats = self::seedEventRegistrationShowcaseSubmissions($schema, $org);
|
|
|
|
if ($command !== null) {
|
|
$draft = $stats['draft_person'];
|
|
if ($draft === null) {
|
|
$command->warn('[FormBuilderDevSeeder] No person with user_id found — draft submission skipped.');
|
|
} else {
|
|
$command->info("[FormBuilderDevSeeder] Draft registration stored for {$draft->full_name} (person {$draft->id})");
|
|
}
|
|
|
|
$submitted = $stats['submitted_person'];
|
|
if ($submitted === null) {
|
|
$command->warn('[FormBuilderDevSeeder] Not enough persons with user_id for submitted demo — §31.10 sync not exercised.');
|
|
} else {
|
|
$command->info("[FormBuilderDevSeeder] Submitted registration for {$submitted->full_name}");
|
|
$command->info(" TAG_PICKER sync result: {$stats['synced_tag_count']} self_reported tags now on user {$submitted->user_id}");
|
|
}
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
|
|
private static function buildDraftSubmission(FormSchema $schema, Person $person): FormSubmission
|
|
{
|
|
$fields = $schema->fields()->get()->keyBy('slug');
|
|
|
|
/** @var FormSubmission $submission */
|
|
$submission = FormSubmission::create([
|
|
'form_schema_id' => $schema->id,
|
|
'subject_type' => 'person',
|
|
'subject_id' => $person->id,
|
|
'submitted_by_user_id' => $person->user_id,
|
|
'status' => FormSubmissionStatus::DRAFT->value,
|
|
'is_test' => false,
|
|
'submitted_in_locale' => 'nl',
|
|
'opened_at' => now()->subDay(),
|
|
]);
|
|
|
|
// Realistic partial fill — deliberately skip TAG_PICKER (that path
|
|
// is exercised by the submitted demo below).
|
|
self::createValueIfField($fields, 'shirtmaat', 'M', $submission);
|
|
self::createValueIfField($fields, 'dieetwensen', ['Vegetarisch'], $submission);
|
|
|
|
return $submission->refresh();
|
|
}
|
|
|
|
/**
|
|
* Full-submit path: create draft, fill values (incl. TAG_PICKER), run
|
|
* the service so FormSubmissionSubmitted fires and §31.10 runs. Queue
|
|
* connection is flipped to sync for the duration so the listener
|
|
* executes inline rather than being deferred to redis during seeding.
|
|
*/
|
|
private static function buildSubmittedSubmission(FormSchema $schema, Organisation $org, Person $person): int
|
|
{
|
|
$tagIds = PersonTag::withoutGlobalScopes()
|
|
->where('organisation_id', $org->id)
|
|
->where('is_active', true)
|
|
->orderBy('sort_order')
|
|
->orderBy('name')
|
|
->take(3)
|
|
->pluck('id')
|
|
->all();
|
|
|
|
/** @var FormSubmissionService $submissionService */
|
|
$submissionService = app(FormSubmissionService::class);
|
|
|
|
$submission = $submissionService->createDraft(
|
|
$schema,
|
|
$person,
|
|
$person->user,
|
|
[
|
|
'opened_at' => now()->subHours(2),
|
|
'is_test' => false,
|
|
],
|
|
);
|
|
|
|
/** @var FormValueService $valueService */
|
|
$valueService = app(FormValueService::class);
|
|
$valueService->upsertMany(
|
|
$submission,
|
|
[
|
|
'shirtmaat' => 'L',
|
|
'dieetwensen' => ['Glutenvrij', 'Lactosevrij'],
|
|
'vaardigheden' => $tagIds,
|
|
'opmerkingen' => 'Kan eerder als nodig.',
|
|
],
|
|
$person->user,
|
|
);
|
|
|
|
// Force synchronous listener execution while we're in the seeder
|
|
// so user_organisation_tags is populated by the time the migrate
|
|
// command returns (see `QUEUE_CONNECTION=redis` in local .env).
|
|
$previousConnection = config('queue.default');
|
|
config(['queue.default' => 'sync']);
|
|
|
|
try {
|
|
$submissionService->submit($submission->refresh(), $person->user);
|
|
} finally {
|
|
config(['queue.default' => $previousConnection]);
|
|
}
|
|
|
|
return UserOrganisationTag::query()
|
|
->where('user_id', $person->user_id)
|
|
->where('organisation_id', $org->id)
|
|
->where('source', 'self_reported')
|
|
->count();
|
|
}
|
|
|
|
private static function uniqueSchemaSlug(Organisation $org, string $base): string
|
|
{
|
|
$candidate = $base;
|
|
$i = 2;
|
|
while (FormSchema::withoutGlobalScopes()
|
|
->where('organisation_id', $org->id)
|
|
->where('slug', $candidate)
|
|
->exists()
|
|
) {
|
|
$candidate = $base.'-'.$i;
|
|
$i++;
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
}
|