Files
crewli/dev-docs/form-builder-migration-playbook.md
bert.hausmans cfc7610497 docs(forms): SCHEMA crosswalk, foundation concept page, getting-started + migration playbook, copy catalogue init
SCHEMA.md
- New §3.5.12 "Form Builder" with the legacy-tables-retained note
  placed prominently directly under the section header (per S1 wrap-up
  Path 3 decision: Phase 8 deferred to S2).
- Crosswalk: every legacy volunteer_profiles column → its new home
  (user_profiles columns vs form_fields vs person_tags).
- Summary table for the 13 new tables with one-line purpose + ARCH §
  pointer each.
- Activity log strategy and multi-tenancy discipline noted.
- §3.5.4 marked SUPERSEDED with a pointer to the new section.

/dev-docs/form-builder-migration-playbook.md (new)
- Operator runbook for forms:migrate-legacy-data on real legacy data.
- Pre-flight audit, dry-run, migrate, verify, spot-check, rollback
  paths spelled out. Same legacy-tables-retained note prominently.

/dev-docs/form-builder-getting-started.md (new)
- Developer onboarding. Mental model, code samples for creating a
  schema/field/submission/value, adding a new subject type, registering
  a custom field type, suppressing activity log via
  App\Support\ActivityLog::suppressed.

/dev-docs/COPY_CATALOGUE.md (new)
- Seeded verbatim from ARCH §30 (naming conventions, tooltip catalogue,
  warning catalogue) with a header explaining purpose, growth strategy,
  and the per-PR update workflow.

/docs/organizer/forms/concepts/wat-is-een-formulier.md (new VitePress)
- Dutch, informal je/jij. Follows /docs/.templates/concept-page.md.
- Three example use-cases: vrijwilligersregistratie, artist advance,
  incidentrapportage. Light foundation; depth arrives in S2-S5.

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

187 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Form Builder — Migration Playbook
> Operator runbook for migrating legacy `registration_form_fields` /
> `person_field_values` / `registration_field_templates` data into the new
> `form_*` structure. Companion to `/dev-docs/ARCH-FORM-BUILDER.md` §11.
---
## Status (post-S1)
**Legacy tables retained intentionally.** The tables `registration_form_fields`,
`person_field_values`, and `registration_field_templates` remain in the
schema through end of S1. They are dropped atomically in the first S2
commit together with the removal of legacy controllers, services, requests,
resources, policies, and routes. DevSeeder and FormBuilderDevSeeder no
longer write to them; they hold zero rows in dev but the schema is preserved
for environments with real legacy data that will be migrated via
`forms:migrate-legacy-data`.
Until S2 lands, the legacy registration UI remains functional for any
environment that depends on it.
---
## When to use this playbook
Run this when promoting an environment that holds real legacy
registration data into the new form-builder schema. For development
machines starting from `migrate:fresh --seed`, no migration is needed —
DevSeeder produces new-structure data directly.
---
## Pre-flight audit
Before touching any data, confirm the working state matches expectations.
```bash
# 1. Working tree clean?
git status
# 2. On the expected branch?
git log -1 --oneline
# 3. Database backup exists? (production / staging only — your call for dev)
# 4. Verify the new tables are present and the legacy tables still hold data
php artisan db:show | grep -E "form_|registration_form_fields|person_field_values|registration_field_templates"
```
If the legacy tables are absent, the migration command will detect this
and exit cleanly without doing work — there is nothing to migrate.
---
## Step 1 — Dry run
Always start with a dry run. It writes nothing and prints the same summary
table the real run would produce.
```bash
php artisan forms:migrate-legacy-data --dry-run
```
**What to look for:**
- The per-organisation log lines list every event that will get a new
schema and the number of fields/submissions per event.
- The summary table shows projected counts for `form_schemas`,
`form_fields`, `form_submissions`, `form_values`, `form_templates`.
- Exit code is `0`.
If anything looks surprising — wildly off counts, organisations missing,
templates duplicated — STOP and investigate. Do not proceed to step 2 until
the dry-run output is what you expect.
---
## Step 2 — Run the migration
```bash
php artisan forms:migrate-legacy-data
```
**What happens:**
- Per organisation, inside a transaction (one bad org rolls back only itself).
- Per event in that org: one `form_schemas` row + N `form_fields` rows.
- Per person with `person_field_values` for that event: one `form_submissions`
row (status = `submitted` if person.status ∈ applied/approved/no_show
else `draft`) + one `form_values` row per legacy value.
- `FormValueObserver` populates typed columns and the
`form_value_options` pivot during the inserts (per ARCH §7.2).
- All `registration_field_templates` rows are wrapped in the ARCH §4.6.1
`schema_snapshot` shape and inserted as `form_templates`.
- The whole run is wrapped in `App\Support\ActivityLog::suppressed(...)`
so the activity log isn't flooded by hundreds of `logSchemaChange` /
`logFieldChange` entries from the bulk inserts.
- After migration completes, verification runs automatically. Final
exit code is whatever `forms:verify-data-integrity` returns.
**Idempotent:** if a `form_schemas` row already exists for `(org, event,
event_registration)`, that event is skipped silently. Safe to re-run.
---
## Step 3 — Verify
Verification runs automatically at the end of step 2. To re-run it
standalone (or to verify state without touching data):
```bash
php artisan forms:verify-data-integrity
```
Or, equivalently:
```bash
php artisan forms:migrate-legacy-data --verify-only
```
**Nine checks:**
1. Schema coherence (purpose enum, custom_purpose_slug ⇄ purpose=custom, public_token uniqueness)
2. Field coherence (FK valid, field_type ∈ FormFieldType custom registry, is_filterable matches type, slug unique within schema, binding JSON valid against `config/form_binding.php`)
3. Submission coherence (subject_type/subject_id consistency, subject_type ∈ `config/form_subjects.php`, status ⇄ submitted_at)
4. Value coherence (orphans, unique pair, length, multi-value sanity, value_indexed only set when field.is_filterable)
5. User profile coherence (every user has one profile; reliability_score ∈ [0, 5])
6. Data migration counts (skipped when legacy tables absent)
7. Orphan records (pivot rows, section statuses)
8. Section/schema relation consistency
9. (`--strict`) Strict reachability: value.field.schema must equal value.submission.schema
Exit code `0` means all checks passed. Anything else is a real failure
worth investigating before proceeding.
---
## Step 4 — Spot-check a sample
After verification is green, eyeball a sample submission to confirm the
shape is right.
```bash
php artisan tinker
>>> use App\Models\FormBuilder\FormSubmission;
>>> $sub = FormSubmission::with(['schema', 'values.field'])->latest()->first();
>>> $sub->schema->name;
>>> $sub->values->map(fn($v) => [$v->field->slug => $v->value]);
```
Compare against the legacy view of the same person's data in
`person_field_values` to confirm nothing was silently lost.
---
## Rollback
The data-migration command does not have a `down()` step. If a migration
went wrong, the recovery path is:
1. Restore from backup (production / staging).
2. For dev: `php artisan migrate:fresh --seed` rebuilds everything from scratch.
The legacy tables are preserved during the migration — they are not
modified. So even after running `forms:migrate-legacy-data`, the legacy
data is still there (until S2 drops the tables). This means a production
data-migration error can be recovered by truncating the new form_* tables
and re-running with the underlying legacy data still intact.
---
## Common gotchas
- **`registration_form_fields` table absent:** the command detects this
and exits 0 silently. Expected behaviour for fresh environments.
- **PII heuristic miss:** the heuristic in §11.2.1 of the ARCH covers
obvious patterns (email, phone, geboort, allergie, …) but is not
exhaustive. After migration, organisers can fine-tune `is_pii` per
field via the form-builder UI (S5).
- **`is_published` derived from event status:** the command considers an
event "published" when its status is one of registration_open / buildup
/ showday / teardown / closed. If the legacy registration was open on
events with other statuses, adjust `is_published` manually after migration.
- **Custom field types:** the command maps the 10 legacy field types
(text/textarea/select/multiselect/checkbox/radio/boolean/number/tag_picker/heading)
into the new uppercase enum values. If your legacy data has values
outside this set, the command will set them to `TEXT` and the verifier
will flag them as invalid `field_type`.