Commit Graph

6 Commits

Author SHA1 Message Date
15e4e49d8c feat(form-field): backfill form_fields.options to form_field_options
Atomic data migration. Every options datum in the database — in
form_fields and form_field_library, their translations bags, and the
form_submissions.schema_snapshot + form_templates.schema_snapshot JSON
blobs — is converted to the new relational rich-shape representation.

Strict dispatch per §17.4.4 / §8.7 convention:
  - Fail on field_type ∉ {RADIO, SELECT, MULTISELECT, CHECKBOX_LIST}
    carrying non-null options (post-WS-5b TAG_PICKER seed-bug indicator)
  - Fail on non-flat-string-array options shape
  - Fail on translations.{locale}.options[] length mismatch
  - Fail on non-string / >255-char translated labels
  - Fail on any residual translations.{locale}.options key after
    step C migration

Snapshot rewrite in-place: both form_submissions.schema_snapshot and
form_templates.schema_snapshot walk fields[*] and rewrite options to
the new rich-shape, strip per-locale options[] from the parallel
translations bag. Zero-compromise directive — no reader tolerance for
pre-WS-5d shape in commit 3 onwards.

Rollback reconstructs JSON column shapes plus translations bags.
Forward+back pair safe as a unit; partial rollback unsupported.

FormFieldService::insertFromLibrary switches from JSON-copy to
FormFieldOptionService::copyOptions row-clone per addendum Q3 row-copy
mandate. The field's own translations bag no longer carries
{locale}.options keys — those live on option rows now.

Seeders and factories switch to service-level option creation:
  - FormBuilderDevSeeder.canonicalFields keeps flat-string options as
    its data shape; FormField::create no longer receives an options
    key, the post-create FormFieldOptionService::replaceOptions call
    inserts the rich rows. The same applies to
    seedEventRegistrationShowcaseSchema. The vergoedingstype field's
    legacy {label, description} object shape (a pre-WS-5d seed-bug
    that the strict backfill would reject) is normalised to flat
    strings; the descriptions are dropped.
  - seedSystemTemplates embeds rich-shape options in the template
    snapshot — no flat-array snapshot data remains in newly-seeded
    rows.
  - FormFieldFactory + FormFieldLibraryFactory drop the options
    default; new ::withOptions() helper accepts either flat strings
    (each becomes value+label) or full spec arrays and routes through
    the service.

JSON columns (form_fields.options, form_field_library.options) remain
present and writable via fillable; column-drop lands in commit 5.
Reads from the JSON column still exist in resources, snapshot writer,
FormRequests, FormValueService, and FilterRegistryController — commit
3 switches those all atomically.

Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new backfill_form_field_options migration on the migration stack.

Tests: 1182 → 1193 green (+11 tests / +56 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:21:26 +02:00
d494478c08 feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns 2026-04-24 22:42:35 +02:00
fedaed1b32 feat(form-builder): form_field_validation_rules table + polymorphic owner + scope + cascade 2026-04-24 22:01:36 +02:00
61719bf8bf refactor(form-builder): pre-publish check reads form_field_bindings; drop binding JSON columns 2026-04-24 20:09:27 +02:00
af8a9da038 feat(form-builder): form_field_bindings table + polymorphic owner + cascade observer
WS-5a commit 1 of 4 per ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 Q3.

Creates the relational home for what was form_fields.binding JSON and
form_field_library.default_binding JSON. Owner discriminator is polymorphic
morph (owner_type/owner_id) — the pattern the rest of WS-5 (5b validation_rules,
5d options) will reuse.

Migration backfills rows from both JSON sources in a single transaction and
is genuinely reversible (rollback reconstructs the JSON). Old columns remain
in place until commit 3 has switched all readers.

Pattern B (binding=null) is represented by absence of row. mode enum covers
entity_owned / mirrored only.

Cascade on owner delete via observer — bindings are physical state, not
historical audit. FormFieldBindingScope enforces multi-tenancy via UNION over
both owner chains (form_field → schema → org OR form_field_library → org) —
Q2's declarative tenantScopeStrategy() can't walk morph parents.

Tests: migration forward/back, morph relation, cascade observer, scope
isolation, enum coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:43:11 +02:00
85815ccb16 feat(forms): add Eloquent models, observer, events, activity-log helpers
Phase 4 of S1.

Models (app/Models/FormBuilder/): FormSchema, FormSchemaSection, FormField,
FormSubmission, FormValue, FormValueOption, FormTemplate, FormFieldLibrary,
FormSchemaWebhook, FormWebhookDelivery, FormSubmissionSectionStatus,
FormSubmissionDelegation. Plus UserProfile at app/Models/ (user-universal).

OrganisationScope applied on: FormSchema, FormTemplate, FormFieldLibrary.
FormSchemaWebhook documents inherited-scope discipline (OrganisationScope's
strategies — organisation_id/event_id/festival_section_id — don't cover
form_schema_id; direct queries would leak across orgs, so must go via
$schema->webhooks()).

User::profile()/getOrCreateProfile(), Event::formSchemas() (morphMany),
Person::formSubmissions() (morphMany).

Morph map enforced in AppServiceProvider with 28 keys covering every model
that appears as activitylog subject/causer. Also updated
OrganisationDashboardService (and its test) to query activitylog via
getMorphClass() instead of FQCN.

Activity log strategy: nuanced explicit calls (logSchemaChange on FormSchema,
logFieldChange on FormField) — no LogsActivity trait. Suppression for bulk
fixtures via App\Support\ActivityLog::suppressed(fn() => ...) which flips
config('activitylog.enabled') around a callback. Both our explicit calls
and spatie's trait on Organisation respect the flag via ActivityLogger::log().

FormValueObserver (app/Observers/FormBuilder/) populates value_indexed/
value_number/value_date/value_bool on save per field.value_storage_hint,
rebuilds form_value_options pivot on multi-value filterable fields, cleans
up on delete. Memoised field cache avoids N+1. Registered in AppServiceProvider.

9 lightweight event classes (app/Events/FormBuilder/) as SerializesModels
containers — submission lifecycle signatures lock in for S2 services, no
listeners yet.

Factories for all models with Dutch fake data (fake('nl_NL')). FormSchema
factory uses defaultSubmissionMode(); FormField factory uses
recommendedValueStorageHint().

Tests: 9 new observer tests (all pass); full suite 910/910 (up from 901).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:35:41 +02:00