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>
187 lines
7.0 KiB
Markdown
187 lines
7.0 KiB
Markdown
# 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`.
|