docs(schema): bump v1.8 → v2.0 with Form Builder tables, drop legacy registration EAV
Reflects the post-S1+S2a+S2b database state. Nothing but SCHEMA.md changed. - Header: Version → 2.0, added v2.0 changelog entry covering the 13 new tables, the 3 dropped legacy tables, the preserved person_section_preferences, organisations.default_locale, and the events.registration_show_* drops. - Table of Contents: updated §3.5.5b name to "Section Preferences", added entries for §3.5.10 Email Infrastructure, §3.5.11 Rules, §3.5.12 Form Builder (which were already in the file but missing from the TOC). - §3.5.1 organisations: added default_locale column (FormLocaleResolver fallback chain, ARCH §16.2). - §3.5.1 events: removed registration_show_section_preferences + registration_show_availability columns with a pointer at form_fields.is_portal_visible / conditional_logic. - §3.5.4: removed the never-created volunteer_profiles table block; the other three tables in that section (volunteer_festival_history, post_festival_evaluations, festival_retrospectives) are unchanged. - §3.5.5b: renamed to "Section Preferences"; design note pointing at events.registration_show_section_preferences replaced with a pointer at form_fields.is_portal_visible / conditional_logic. - §3.5.9: renamed to "Check-In & Operational"; removed the never-created public_forms stub and the colliding legacy form_submissions block (both documented planned-but-never-created tables) with a short note pointing at the Form Builder as the home for form concepts. Flagged separately below because it's technically beyond the task's explicit scope but unavoidable (SCHEMA.md would otherwise describe two different tables under the same name). - §3.5.12 Form Builder: summary replaced with full per-table documentation for all 13 tables in the ARCH §4 order — user_profiles, form_schemas (polymorphic owner, public_token rotation with public_token_previous + public_token_rotated_at, edit_lock_*), form_schema_sections, form_field_library, form_fields, form_submissions, form_submission_section_statuses, form_submission_delegations, form_values (observer-driven typed columns value_indexed/number/date/bool and form_value_options multi-value rebuild per ARCH §7.2), form_value_options, form_templates, form_schema_webhooks, form_webhook_deliveries. Added short notes on activity log strategy and the §31.10 FORM-02 tag-sync listener. Migrations-vs-ARCH discrepancies (migrations win, per CLAUDE.md): - form_values carries created_at / updated_at timestamps, though ARCH §4.4 does not list them. Documented as present. - form_webhook_deliveries has no timestamps columns; last_attempt_at is the effective timestamp. Documented as such. - form_schema_webhooks stores url / secret as encrypted TEXT columns (Eloquent-cast encryption); ARCH says "encrypted" without specifying. Documented the column type. - public_forms + legacy form_submissions documented in §3.5.9 never existed in the DB (confirmed via Schema::hasTable). Removed those doc stubs; the naming collision with the new Form Builder form_submissions made leaving them in place a correctness hazard.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Crewli — Core Database Schema
|
||||
|
||||
> Source: Design Document v1.3 — Section 3.5
|
||||
> **Version: 1.8** — Updated April 2026
|
||||
> **Version: 2.0** — Updated April 2026
|
||||
>
|
||||
> **Changelog:**
|
||||
>
|
||||
@@ -26,6 +26,29 @@
|
||||
> Removed minimum volunteer hours threshold concept. Removed hardcoded
|
||||
> motivation form step. Moved payment status from fixed admin field to
|
||||
> dynamic registration field.
|
||||
> - v2.0: Universal Form Builder replaces event-scoped registration EAV
|
||||
> (S1 + S2a + S2b landed). Full architecture:
|
||||
> `/dev-docs/ARCH-FORM-BUILDER.md` v1.2.
|
||||
> **Added tables (13):** `user_profiles`, `form_schemas`,
|
||||
> `form_schema_sections`, `form_field_library`, `form_fields`,
|
||||
> `form_submissions`, `form_submission_section_statuses`,
|
||||
> `form_submission_delegations`, `form_values`, `form_value_options`,
|
||||
> `form_templates`, `form_schema_webhooks`, `form_webhook_deliveries`.
|
||||
> **Dropped tables (3):** `registration_form_fields`, `person_field_values`,
|
||||
> `registration_field_templates`.
|
||||
> **Preserved:** `person_section_preferences` — still the target for the
|
||||
> Form Builder's `SECTION_PRIORITY` field type (ARCH §31.3).
|
||||
> **Added column:** `organisations.default_locale` (default `'nl'`) —
|
||||
> last link in the `FormLocaleResolver` fallback chain (ARCH §16.2).
|
||||
> **Dropped columns on `events`:** `registration_show_section_preferences`,
|
||||
> `registration_show_availability` (now expressed as
|
||||
> `form_fields.is_portal_visible` / `conditional_logic`).
|
||||
> **Renamed concept:** `volunteer_profiles` (planning placeholder, never
|
||||
> physically created) is retired; user-universal columns live on the new
|
||||
> `user_profiles` table and event-variable / skill columns moved to
|
||||
> `form_fields` + `person_tags`. The `§3.5.9 form_submissions` stub
|
||||
> (tied to the never-created `public_forms` concept) is retired in favour
|
||||
> of the Form Builder `form_submissions` table described in §3.5.12.
|
||||
|
||||
---
|
||||
|
||||
@@ -45,17 +68,19 @@
|
||||
1. [3.5.1 Foundation](#351-foundation)
|
||||
2. [3.5.1a Multi-Factor Authentication](#351a-multi-factor-authentication)
|
||||
3. [3.5.2 Locations](#352-locations)
|
||||
3. [3.5.3 Festival Sections, Time Slots & Shifts](#353-festival-sections-time-slots--shifts)
|
||||
4. [3.5.4 Volunteer Profile & History](#354-volunteer-profile--history)
|
||||
5. [3.5.5 Crowd Types, Persons & Crowd Lists](#355-crowd-types-persons--crowd-lists)
|
||||
6. [3.5.5a Person Tags & Skills](#355a-person-tags--skills)
|
||||
7. [3.5.5b Registration Form Fields & Section Preferences](#355b-registration-form-fields--section-preferences)
|
||||
8. [3.5.5c Person Identity Matching](#355c-person-identity-matching)
|
||||
9. [3.5.6 Accreditation Engine](#356-accreditation-engine)
|
||||
10. [3.5.7 Artists & Advancing](#357-artists--advancing)
|
||||
11. [3.5.8 Communication & Briefings](#358-communication--briefings)
|
||||
12. [3.5.9 Forms, Check-In & Operational](#359-forms-check-in--operational)
|
||||
13. [3.5.11 Database Design Rules & Index Strategy](#3511-database-design-rules--index-strategy)
|
||||
4. [3.5.3 Festival Sections, Time Slots & Shifts](#353-festival-sections-time-slots--shifts)
|
||||
5. [3.5.4 Volunteer Profile & History](#354-volunteer-profile--history)
|
||||
6. [3.5.5 Crowd Types, Persons & Crowd Lists](#355-crowd-types-persons--crowd-lists)
|
||||
7. [3.5.5a Person Tags & Skills](#355a-person-tags--skills)
|
||||
8. [3.5.5b Section Preferences](#355b-section-preferences)
|
||||
9. [3.5.5c Person Identity Matching](#355c-person-identity-matching)
|
||||
10. [3.5.6 Accreditation Engine](#356-accreditation-engine)
|
||||
11. [3.5.7 Artists & Advancing](#357-artists--advancing)
|
||||
12. [3.5.8 Communication & Briefings](#358-communication--briefings)
|
||||
13. [3.5.9 Check-In & Operational](#359-check-in--operational)
|
||||
14. [3.5.10 Email Infrastructure](#3510-email-infrastructure)
|
||||
15. [3.5.11 Database Design Rules & Index Strategy](#3511-database-design-rules--index-strategy)
|
||||
16. [3.5.12 Form Builder](#3512-form-builder)
|
||||
|
||||
---
|
||||
|
||||
@@ -88,15 +113,16 @@
|
||||
|
||||
### `organisations`
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ---------------- | ------------------ | -------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `name` | string | |
|
||||
| `slug` | string | unique |
|
||||
| `billing_status` | enum | `trial\|active\|suspended\|cancelled` |
|
||||
| `settings` | JSON | Display prefs only — no queryable data |
|
||||
| `created_at` | timestamp | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
| Column | Type | Notes |
|
||||
| ------------------ | ------------------ | ------------------------------------------------------------------ |
|
||||
| `id` | ULID | PK |
|
||||
| `name` | string | |
|
||||
| `slug` | string | unique |
|
||||
| `billing_status` | enum | `trial\|active\|suspended\|cancelled` |
|
||||
| `default_locale` | string(10) | **v2.0** default: `'nl'`. Fallback for `FormLocaleResolver` chain (ARCH §16.2) when `users.locale` and `form_schemas.locale` are absent. |
|
||||
| `settings` | JSON | Display prefs only — no queryable data |
|
||||
| `created_at` | timestamp | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
|
||||
**Relations:** `hasMany` events, crowd_types, accreditation_categories
|
||||
**Soft delete:** yes
|
||||
@@ -169,10 +195,13 @@
|
||||
| `is_recurring` | bool | **v1.7** default: false. True = generated from recurrence rule |
|
||||
| `recurrence_rule` | string nullable | **v1.7** RRULE (RFC 5545): "FREQ=WEEKLY;BYDAY=SA,SU;UNTIL=20270126" |
|
||||
| `recurrence_exceptions` | JSON nullable | **v1.7** Array of {date, type: cancelled\|modified, overrides: {}}. JSON OK: opaque config |
|
||||
| `registration_show_section_preferences` | bool | **v1.8** Default: true. Toggle section preferences step in registration form |
|
||||
| `registration_show_availability` | bool | **v1.8** Default: true. Toggle time slot availability step in registration form |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
|
||||
> **v2.0 — removed:** `registration_show_section_preferences` and
|
||||
> `registration_show_availability` are gone; the equivalent behaviour now
|
||||
> lives on `form_fields.is_portal_visible` plus per-field
|
||||
> `conditional_logic` in the Form Builder (§3.5.12).
|
||||
|
||||
**Relations:**
|
||||
|
||||
- `belongsTo` Organisation
|
||||
@@ -669,33 +698,13 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
|
||||
|
||||
## 3.5.4 Volunteer Profile & History
|
||||
|
||||
> **SUPERSEDED by S1 of the form-builder refactor.** The old
|
||||
> `volunteer_profiles` table was a planning placeholder that was never
|
||||
> physically created. It has been split: user-universal columns moved
|
||||
> to the new `user_profiles` table (§3.5.12), and event-variable / skill
|
||||
> columns moved to `form_fields` and `person_tags`. See §3.5.12 for the
|
||||
> new structure and the column-by-column crosswalk.
|
||||
|
||||
### `volunteer_profiles`
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------------- | --------------- | ------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `user_id` | ULID FK unique | → users — 1:1 |
|
||||
| `bio` | text nullable | |
|
||||
| `photo_url` | string nullable | |
|
||||
| `tshirt_size` | string nullable | |
|
||||
| `first_aid` | bool | |
|
||||
| `driving_licence` | bool | |
|
||||
| `allergies` | text nullable | |
|
||||
| `emergency_contact_name` | string nullable | |
|
||||
| `emergency_contact_phone` | string nullable | |
|
||||
| `reliability_score` | decimal(3,2) | 0.00–5.00, computed via scheduled job |
|
||||
| `is_ambassador` | bool | |
|
||||
|
||||
**Unique constraint:** `UNIQUE(user_id)`
|
||||
|
||||
---
|
||||
> **v2.0 — `volunteer_profiles` retired.** The old table was a planning
|
||||
> placeholder that was never physically created. User-universal columns
|
||||
> (bio, photo, emergency contact, reliability_score, is_ambassador) live on
|
||||
> the new `user_profiles` table (§3.5.12). Event-variable / skill-like
|
||||
> columns moved to `form_fields` (per-event, via the Form Builder) and
|
||||
> `person_tags`. See §3.5.12 for the new table + the column-by-column
|
||||
> crosswalk.
|
||||
|
||||
### `volunteer_festival_history`
|
||||
|
||||
@@ -1334,39 +1343,15 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
|
||||
|
||||
---
|
||||
|
||||
## 3.5.9 Forms, Check-In & Operational
|
||||
## 3.5.9 Check-In & Operational
|
||||
|
||||
### `public_forms`
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ----------------------------- | ------------- | -------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `event_id` | ULID FK | → events |
|
||||
| `name` | string | |
|
||||
| `crowd_type_id` | ULID FK | → crowd_types |
|
||||
| `fields` | JSON | Form config — not filtered |
|
||||
| `conditional_logic` | JSON | Form config — not filtered |
|
||||
| `iframe_token` | ULID unique | |
|
||||
| `confirmation_email_template` | text nullable | |
|
||||
| `is_active` | bool | |
|
||||
|
||||
**Indexes:** `(event_id, crowd_type_id, is_active)`
|
||||
|
||||
---
|
||||
|
||||
### `form_submissions`
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ---------------- | --------- | ----------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `public_form_id` | ULID FK | → public_forms |
|
||||
| `person_id` | ULID FK | → persons |
|
||||
| `data` | JSON | Free form results |
|
||||
| `submitted_at` | timestamp | |
|
||||
|
||||
**Indexes:** `(public_form_id, submitted_at)`, `(person_id)`
|
||||
|
||||
---
|
||||
> **v2.0 — moved:** the former `public_forms` + stub `form_submissions`
|
||||
> entries here described a never-created iframe-embed feature (FORM-01 in
|
||||
> BACKLOG). The Form Builder (§3.5.12) now owns every form concept:
|
||||
> `form_schemas` with `public_token` handle public-facing forms,
|
||||
> `form_submissions` stores results. If the iframe-embed requirement
|
||||
> returns it will layer on top of those tables rather than ship a new
|
||||
> `public_forms` table.
|
||||
|
||||
### `check_ins`
|
||||
|
||||
@@ -1568,21 +1553,23 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
|
||||
|
||||
---
|
||||
|
||||
## 3.5.5b Section Preferences (form-builder integration)
|
||||
## 3.5.5b Section Preferences
|
||||
|
||||
> The legacy `registration_form_fields`, `person_field_values`, and
|
||||
> `registration_field_templates` tables were dropped in S2a. Their
|
||||
> replacement is the Form Builder schema described in §3.5.12.
|
||||
> Dynamic registration fields moved to Form Builder — see §3.5.10
|
||||
> (renumbered to §3.5.12 in v2.0). The legacy `registration_form_fields`,
|
||||
> `person_field_values`, and `registration_field_templates` tables were
|
||||
> dropped in S2a.
|
||||
>
|
||||
> `person_section_preferences` is retained — it remains the integration
|
||||
> target for the Form Builder's SECTION_PRIORITY field type (ARCH §31.3).
|
||||
> target for the Form Builder's `SECTION_PRIORITY` field type (ARCH §31.3).
|
||||
|
||||
### `person_section_preferences`
|
||||
|
||||
> Volunteer's preferred sections for shift assignment. Soft hints for the
|
||||
> organiser, NOT promises. The organiser retains full flexibility to assign
|
||||
> anyone anywhere. Captured during registration (configurable per event
|
||||
> via `events.registration_show_section_preferences`).
|
||||
> organiser, NOT promises. The organiser retains full flexibility to
|
||||
> assign anyone anywhere. Captured by the Form Builder's
|
||||
> `SECTION_PRIORITY` field type on an `event_registration` schema; the
|
||||
> listener per ARCH §31.3 upserts rows here on submit.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --------------------- | ------- | --------------------------- |
|
||||
@@ -1596,10 +1583,13 @@ $effectiveDate = $shift->end_date ?? $shift->timeSlot->date;
|
||||
|
||||
Design notes:
|
||||
- Priority is a ranking, not a score. 1 = first choice.
|
||||
- Accompanying text in form: "We proberen hier zoveel mogelijk rekening mee te
|
||||
houden, maar de uiteindelijke indeling wordt bepaald door de organisatie."
|
||||
- For festivals: shown sections = sub-event sections + parent's cross_event sections.
|
||||
- Only shown when `events.registration_show_section_preferences = true`.
|
||||
- Accompanying text in form: "We proberen hier zoveel mogelijk rekening
|
||||
mee te houden, maar de uiteindelijke indeling wordt bepaald door de
|
||||
organisatie."
|
||||
- For festivals: shown sections = sub-event sections + parent's
|
||||
`cross_event` sections.
|
||||
- Whether the step renders is driven by the form schema — `form_fields.is_portal_visible`
|
||||
+ `conditional_logic` — not by a column on `events`.
|
||||
|
||||
---
|
||||
|
||||
@@ -1790,20 +1780,21 @@ Immutable audit record of every email sent. No soft deletes.
|
||||
|
||||
## 3.5.12 Form Builder
|
||||
|
||||
> See `/dev-docs/ARCH-FORM-BUILDER.md` v1.2 for the authoritative
|
||||
> specification. This SCHEMA.md section is a summary only and will be
|
||||
> fully rewritten at the end of S6.
|
||||
> Universal form builder — one schema/field/submission/value stack serving
|
||||
> 22 `FormPurpose` variants from `event_registration` to `incident_report`
|
||||
> to `signature_contract`. See `/dev-docs/ARCH-FORM-BUILDER.md` v1.2 for
|
||||
> the authoritative behaviour spec; this section documents the physical
|
||||
> tables as landed through S1 + S2a + S2b. Where ARCH §4 and the
|
||||
> migrations disagree, the migrations win (document code-as-built).
|
||||
>
|
||||
> **Legacy tables dropped (S2a).** The tables `registration_form_fields`,
|
||||
> `person_field_values`, and `registration_field_templates` were dropped
|
||||
> by migration `2026_04_20_100000_drop_remaining_legacy_registration_tables`
|
||||
> together with the removal of legacy controllers, services, requests,
|
||||
> resources, policies, routes, and models. The migration's `down()` is a
|
||||
> one-way failure — restoration requires `migrate:fresh` or a pre-S2a
|
||||
> backup. Environments with real legacy data must run
|
||||
> `forms:migrate-legacy-data` BEFORE applying the S2a migration.
|
||||
> **Legacy tables dropped (S2a):** `registration_form_fields`,
|
||||
> `person_field_values`, `registration_field_templates` removed by
|
||||
> `2026_04_20_100000_drop_remaining_legacy_registration_tables`. That
|
||||
> migration's `down()` is a hard failure — restoration requires
|
||||
> `migrate:fresh` or a pre-S2a backup; environments with real legacy
|
||||
> data must run `forms:migrate-legacy-data` BEFORE applying the drop.
|
||||
|
||||
**Crosswalk: legacy `volunteer_profiles` → new locations**
|
||||
**Crosswalk: legacy `volunteer_profiles` (never physically created) → new locations**
|
||||
|
||||
| Legacy column | New location |
|
||||
| ------------------------------ | ----------------------------------------------- |
|
||||
@@ -1819,24 +1810,448 @@ Immutable audit record of every email sent. No soft deletes.
|
||||
| `reliability_score` | `user_profiles.reliability_score` |
|
||||
| `is_ambassador` | `user_profiles.is_ambassador` |
|
||||
|
||||
### New tables (S1)
|
||||
---
|
||||
|
||||
| Table | Purpose |
|
||||
| --- | --- |
|
||||
| `user_profiles` | User-universal profile (bio, photo, emergency contact, reliability_score, is_ambassador, opaque settings JSON). 1:1 with `users`, auto-created by `UserObserver`. ARCH §4.13. |
|
||||
| `form_schemas` | Form definition. Polymorphic owner (event / user_profile / artist / company / null). Carries purpose, submission_mode, is_published, public_token, schema_snapshot policy, freeze_on_submit, retention/consent. ARCH §4.1. |
|
||||
| `form_schema_sections` | Optional sections within a schema (used when `section_level_submit=true`). ARCH §4.8. |
|
||||
| `form_field_library` | Reusable cross-schema field definitions. ARCH §4.7. |
|
||||
| `form_fields` | Field within a schema. `field_type` stored as string (custom types via `CustomFieldTypeRegistry`). Carries binding (Pattern A/B/C), is_filterable, is_pii, conditional_logic, value_storage_hint. ARCH §4.2. |
|
||||
| `form_submissions` | One submission per `(schema, subject)`. Polymorphic subject. Carries status, review_status, schema_snapshot copy, submission lifecycle timestamps, search_index. ARCH §4.3. |
|
||||
| `form_submission_section_statuses` | Per-section status when `section_level_submit=true`. ARCH §4.9. |
|
||||
| `form_submission_delegations` | "X fills in this submission on behalf of Y". ARCH §4.10. |
|
||||
| `form_values` | EAV row per `(submission, field)`. Integer AI PK for fast joins. Typed columns (value_indexed/number/date/bool) populated by `FormValueObserver`. ARCH §4.4. |
|
||||
| `form_value_options` | Filter pivot for multi-value fields (MULTISELECT/CHECKBOX_LIST/TAG_PICKER). Rebuilt by observer. ARCH §4.5. |
|
||||
| `form_templates` | Org-scoped reusable schema snapshots. `is_system` for shipped templates. ARCH §4.6. |
|
||||
| `form_schema_webhooks` | Webhook subscriptions per schema. URL/secret encrypted via Eloquent cast. ARCH §4.11. |
|
||||
| `form_webhook_deliveries` | Webhook delivery audit + retry queue. ARCH §4.12. |
|
||||
### `user_profiles`
|
||||
|
||||
**Activity log strategy:** explicit calls via `FormSchema::logSchemaChange()` and `FormField::logFieldChange()` — no `LogsActivity` trait (would produce noise). Only impactful events logged (publish toggle, purpose change, binding change, is_pii toggle, etc.). Bulk-fixture suppression via `App\Support\ActivityLog::suppressed(fn() => …)` which flips `config('activitylog.enabled')` for the callback.
|
||||
> User-universal profile — replaces the never-created
|
||||
> `volunteer_profiles` placeholder. Holds bio, photo, emergency contact
|
||||
> details, reliability score, ambassador flag, and an opaque UI/
|
||||
> notification `settings` JSON. 1:1 with `users`; auto-created by
|
||||
> `UserObserver` on user creation and backfilled for existing users via
|
||||
> `2026_04_19_100001_populate_user_profiles_from_existing_users`.
|
||||
> Cascade-deleted with the user — no independent soft delete. ARCH §4.13.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK, `HasUlids` trait |
|
||||
| `user_id` | ULID FK | → users, cascade delete, UNIQUE (1:1) |
|
||||
| `bio` | text nullable | |
|
||||
| `photo_url` | string nullable | |
|
||||
| `emergency_contact_name` | string nullable | PII |
|
||||
| `emergency_contact_phone` | string nullable | PII |
|
||||
| `reliability_score` | decimal(3,2) | default: `0.00`. System-computed; not mass-assignable |
|
||||
| `is_ambassador` | bool | default: false. System-awarded; not mass-assignable |
|
||||
| `settings` | JSON nullable | Whitelisted keys only (`ui.*`, `notifications.*`); enforced by `UserProfileSettingsValidator` (ARCH §4.13.1) |
|
||||
| `created_at` | timestamp | |
|
||||
| `updated_at` | timestamp | |
|
||||
|
||||
**Relations:** `belongsTo` User. Exposes a `last_submitted_at` accessor
|
||||
that aggregates the user's submitted, non-test `form_submissions`.
|
||||
**Indexes:** `(reliability_score)`
|
||||
**Unique constraint:** `UNIQUE(user_id)`
|
||||
**Soft delete:** no — cascades on user delete
|
||||
|
||||
---
|
||||
|
||||
### `form_schemas`
|
||||
|
||||
> Form definition. Polymorphic owner (`event` / `user_profile` /
|
||||
> `artist` / `company` / `organisation` / null). Carries purpose, mode,
|
||||
> publication flag, public-token rotation state, snapshot policy,
|
||||
> freeze-on-submit lock, retention/consent, and collaborative
|
||||
> edit-lock columns. `OrganisationScope` applied as a global scope.
|
||||
> ARCH §4.1.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | ------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `organisation_id` | ULID FK | → organisations, cascade delete |
|
||||
| `owner_type` | string(50) nullable | polymorph: `event` / `user_profile` / `artist` / `company` / `organisation` / null |
|
||||
| `owner_id` | ULID nullable | polymorph target |
|
||||
| `name` | string | |
|
||||
| `slug` | string | canonical within organisation |
|
||||
| `purpose` | string(50) | `FormPurpose` enum value |
|
||||
| `custom_purpose_slug` | string nullable | required when `purpose = custom` |
|
||||
| `description` | text nullable | |
|
||||
| `is_published` | bool | default: false |
|
||||
| `submission_mode` | string(20) | `FormSubmissionMode` enum value |
|
||||
| `public_token` | ULID nullable | public-facing form URL token |
|
||||
| `public_token_previous` | ULID nullable | previous token — accepted during the rotation grace window |
|
||||
| `public_token_rotated_at` | timestamp nullable | rotation stamp; grace expires 7 days later |
|
||||
| `submission_deadline` | timestamp nullable | |
|
||||
| `locale` | string(10) | default: `'nl'`; primary locale (translations in `form_fields.translations`) |
|
||||
| `settings` | JSON nullable | Opaque UI config |
|
||||
| `version` | int unsigned | default: 1; bumped on structural edits |
|
||||
| `snapshot_mode` | string(20) | `never` / `on_submit` / `always` (default `never`) |
|
||||
| `freeze_on_submit` | bool | default: false. After first submitted submission, schema-structural edits are blocked |
|
||||
| `retention_days` | int unsigned null | After `submitted_at + retention_days`, PII anonymised via `FormSubmissionAnonymisationService` |
|
||||
| `consent_version` | string nullable | e.g. `"privacy-v2"` |
|
||||
| `section_level_submit` | bool | default: false. Enables sections-with-own-submit flow |
|
||||
| `auto_save_enabled` | bool | default: false |
|
||||
| `max_submissions` | int unsigned null | Optional cap (public schemas) |
|
||||
| `created_by_user_id` | ULID FK nullable | → users, null on delete |
|
||||
| `last_updated_by_user_id` | ULID FK nullable | → users, null on delete |
|
||||
| `edit_lock_user_id` | ULID FK nullable | → users, null on delete. Pessimistic editor lock (ARCH §14.5) |
|
||||
| `edit_lock_expires_at` | timestamp nullable | Lock expires here; auto-released by acquire attempts after expiry |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
|
||||
**Relations:** `belongsTo` organisation, createdBy/lastUpdatedBy/editLockUser (User); `morphsTo` owner; `hasMany` fields, sections, submissions, webhooks
|
||||
**Indexes:** `(organisation_id, purpose)`, `(owner_type, owner_id)`, `(public_token)`, `(public_token_previous)`, `(custom_purpose_slug)`
|
||||
**Unique constraint:** `UNIQUE(organisation_id, slug)`
|
||||
**Global scope:** `OrganisationScope`
|
||||
**Soft delete:** yes
|
||||
|
||||
---
|
||||
|
||||
### `form_schema_sections`
|
||||
|
||||
> Optional sections within a schema, active when
|
||||
> `form_schemas.section_level_submit = true`. Supports dependencies:
|
||||
> a section is gated until its `depends_on_section_id` chain reaches
|
||||
> `approved`. Cycle detection applied on save. ARCH §4.8 + §4.8.1.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ---------------------------- | ------------------ | ----------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_id` | ULID FK | → form_schemas, cascade delete |
|
||||
| `slug` | string | Snapshot-portable section reference |
|
||||
| `name` | string | |
|
||||
| `description` | text nullable | |
|
||||
| `sort_order` | int unsigned | default: 0 |
|
||||
| `submit_independent` | bool | default: true |
|
||||
| `depends_on_section_id` | ULID FK nullable | → form_schema_sections (self), null on delete |
|
||||
| `required_for_schema_submit` | bool | default: true |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
|
||||
**Relations:** `belongsTo` schema; `belongsTo` parent section; `hasMany` form_fields
|
||||
**Indexes:** `(form_schema_id, sort_order)`
|
||||
**Unique constraint:** `UNIQUE(form_schema_id, slug)`
|
||||
**Soft delete:** yes
|
||||
|
||||
---
|
||||
|
||||
### `form_field_library`
|
||||
|
||||
> Cross-schema reusable field definitions scoped per organisation.
|
||||
> Inserting a library field into a schema creates an independent
|
||||
> `form_fields` row linked back via `library_field_id` for analytics.
|
||||
> `OrganisationScope` applied. ARCH §4.7.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | --------------- | -------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `organisation_id` | ULID FK | → organisations, cascade delete |
|
||||
| `name` | string | |
|
||||
| `slug` | string | |
|
||||
| `field_type` | string(50) | One of `FormFieldType` or a registered custom type |
|
||||
| `label` | string | |
|
||||
| `help_text` | text nullable | |
|
||||
| `options` | JSON nullable | Choice options |
|
||||
| `validation_rules` | JSON nullable | |
|
||||
| `default_is_required` | bool | default: false |
|
||||
| `default_is_filterable` | bool | default: false |
|
||||
| `default_binding` | JSON nullable | Pattern A / B / C (see ARCH §6) |
|
||||
| `translations` | JSON nullable | Per-locale overrides |
|
||||
| `description` | text nullable | Admin-only description of intended use |
|
||||
| `usage_count` | int unsigned | default: 0; incremented by `FormFieldService::insertFromLibrary` |
|
||||
| `is_system` | bool | default: false; system-seeded rows flip true |
|
||||
| `is_active` | bool | default: true |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Relations:** `belongsTo` organisation; `hasMany` form_fields via `library_field_id`
|
||||
**Indexes:** `(organisation_id, field_type)`, `(organisation_id, is_active)`
|
||||
**Unique constraint:** `UNIQUE(organisation_id, slug)`
|
||||
**Global scope:** `OrganisationScope`
|
||||
**Soft delete:** no
|
||||
|
||||
---
|
||||
|
||||
### `form_fields`
|
||||
|
||||
> Field within a schema. `field_type` stored as string (not DB enum) so
|
||||
> `CustomFieldTypeRegistry` can extend the catalogue at runtime.
|
||||
> Carries the binding (Pattern A/B/C per ARCH §6), filterability,
|
||||
> PII flag, conditional_logic rules, per-field role restrictions,
|
||||
> translations, and the storage hint that guides
|
||||
> `FormValueObserver`. Uniqueness of `slug` within a schema is
|
||||
> application-enforced (soft deletes prevent a DB-level partial unique).
|
||||
> ARCH §4.2.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_id` | ULID FK | → form_schemas, cascade delete |
|
||||
| `form_schema_section_id` | ULID FK nullable | → form_schema_sections, null on delete |
|
||||
| `library_field_id` | ULID FK nullable | → form_field_library, null on delete |
|
||||
| `field_type` | string(50) | `FormFieldType` enum value OR a key from `config('form_builder.custom_field_types')` |
|
||||
| `slug` | string(100) | Unique per schema (application-enforced) |
|
||||
| `label` | string | Default-locale label |
|
||||
| `help_text` | text nullable | |
|
||||
| `section` | string(100) null | Visual grouping header (independent of `form_schema_section_id`) |
|
||||
| `options` | JSON nullable | Choice options |
|
||||
| `validation_rules` | JSON nullable | min/max/regex/allowed_mime_types/storage_disk/callback, etc. |
|
||||
| `is_required` | bool | default: false |
|
||||
| `is_filterable` | bool | default: false — populates `form_values.value_indexed` / pivot |
|
||||
| `is_portal_visible` | bool | default: true |
|
||||
| `is_admin_only` | bool | default: false — convenience for common role restriction |
|
||||
| `is_unique` | bool | default: false — uniqueness enforced in `FormValueService` (ARCH §4.2.1) |
|
||||
| `is_pii` | bool | default: false — drives retention + anonymisation |
|
||||
| `display_width` | string(10) | default: `full`; `FormFieldDisplayWidth` enum |
|
||||
| `binding` | JSON nullable | Pattern A/B/C binding to entity column (ARCH §6) |
|
||||
| `conditional_logic` | JSON nullable | show_when rules; cycle detection on save |
|
||||
| `role_restrictions` | JSON nullable | Per-field RBAC driving `FieldAccessService` |
|
||||
| `translations` | JSON nullable | `{ <locale>: { label, help_text, options } }` |
|
||||
| `value_storage_hint` | string(10) | default: `json`. `FormValueStorageHint` enum — guides typed-column population |
|
||||
| `review_required` | bool | default: false |
|
||||
| `sort_order` | int unsigned | default: 0 |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete preserves history |
|
||||
|
||||
**Relations:** `belongsTo` schema, section (nullable), libraryField; `hasMany` form_values
|
||||
**Indexes:** `(form_schema_id, sort_order)`, `(form_schema_id, is_filterable)`, `(library_field_id)`, `(form_schema_id, slug)`
|
||||
**Soft delete:** yes
|
||||
|
||||
---
|
||||
|
||||
### `form_submissions`
|
||||
|
||||
> One submission per `(schema, subject)` in `single` / `draft_single`
|
||||
> modes; unbounded in `multiple` mode. Polymorphic `subject_type` /
|
||||
> `subject_id` (allowed types per `config/form_subjects.php`).
|
||||
> Carries the lifecycle timestamps, review status, optional schema
|
||||
> snapshot (when `form_schemas.snapshot_mode != 'never'`), locale used,
|
||||
> idempotency key, anonymisation marker, and a `search_index` text
|
||||
> column fed by FULLTEXT on MySQL and by LIKE fallback elsewhere.
|
||||
> ARCH §4.3.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------------------------- | ------------------ | ---------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_id` | ULID FK | → form_schemas, cascade delete |
|
||||
| `subject_type` | string(50) null | polymorph |
|
||||
| `subject_id` | ULID nullable | polymorph target |
|
||||
| `submitted_by_user_id` | ULID FK nullable | → users, null on delete |
|
||||
| `public_submitter_name` | string nullable | Public submitters |
|
||||
| `public_submitter_email` | string nullable | Public submitters |
|
||||
| `public_submitter_ip` | string(45) null | IPv4/v6 |
|
||||
| `public_submitter_ip_anonymised_at` | timestamp nullable | Set by retention job (ARCH §10.3) |
|
||||
| `status` | string(20) | `FormSubmissionStatus` enum |
|
||||
| `review_status` | string(30) null | `FormSubmissionReviewStatus` enum |
|
||||
| `reviewed_by_user_id` | ULID FK nullable | → users, null on delete |
|
||||
| `reviewed_at` | timestamp nullable | |
|
||||
| `review_notes` | text nullable | |
|
||||
| `submitted_at` | timestamp nullable | |
|
||||
| `schema_version_at_submit` | int unsigned null | `form_schemas.version` at submit time |
|
||||
| `schema_snapshot` | JSON nullable | Full snapshot when policy dictates (ARCH §4.6.1 shape) |
|
||||
| `is_test` | bool | default: false — excluded from reporting & retention |
|
||||
| `submitted_in_locale` | string(10) null | Locale the submitter used while filling in |
|
||||
| `opened_at` | timestamp nullable | First GET |
|
||||
| `first_interacted_at` | timestamp nullable | First field focus |
|
||||
| `submission_duration_seconds` | int unsigned null | opened_at → submitted_at |
|
||||
| `auto_save_count` | int unsigned | default: 0 |
|
||||
| `idempotency_key` | ULID nullable | Duplicate-submit guard |
|
||||
| `anonymised_at` | timestamp nullable | |
|
||||
| `search_index` | mediumText null | Concatenated text of text-type values; FULLTEXT-indexed on MySQL when supported |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
| `deleted_at` | timestamp nullable | Soft delete |
|
||||
|
||||
**Relations:** `belongsTo` schema, submittedBy / reviewedBy (User); `morphsTo` subject; `hasMany` values, section statuses, delegations
|
||||
**Indexes:** `(form_schema_id, status)`, `(subject_type, subject_id)`, `(submitted_by_user_id)`, `(form_schema_id, review_status)`, `(form_schema_id, idempotency_key)`, `FULLTEXT(search_index)` (MySQL/InnoDB — best-effort, skipped gracefully on SQLite)
|
||||
**Events fired:** `FormSubmissionCreated`, `FormSubmissionDraftUpdated`, `FormSubmissionSubmitted`, `FormSubmissionReviewed`, `FormSubmissionSectionSubmitted`, `FormSubmissionSectionReviewed`, `FormSubmissionAnonymised`, `FormSubmissionArchived`, `FormSubmissionDeleted`
|
||||
**Soft delete:** yes
|
||||
|
||||
---
|
||||
|
||||
### `form_submission_section_statuses`
|
||||
|
||||
> Per-section lifecycle state when `form_schemas.section_level_submit =
|
||||
> true`. Integer AI PK — this is a high-volume pivot. ARCH §4.9.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | ------------------ | ------------------------------------------------ |
|
||||
| `id` | int AI | PK |
|
||||
| `form_submission_id` | ULID FK | → form_submissions, cascade delete |
|
||||
| `form_schema_section_id` | ULID FK | → form_schema_sections, cascade delete |
|
||||
| `status` | string(30) | `draft` / `submitted` / `approved` / `rejected` / `changes_requested` |
|
||||
| `submitted_at` | timestamp nullable | |
|
||||
| `reviewed_by_user_id` | ULID FK nullable | → users, null on delete |
|
||||
| `reviewed_at` | timestamp nullable | |
|
||||
| `review_notes` | text nullable | |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Unique constraint:** `UNIQUE(form_submission_id, form_schema_section_id)`
|
||||
**Soft delete:** no
|
||||
|
||||
---
|
||||
|
||||
### `form_submission_delegations`
|
||||
|
||||
> "X fills in this submission on behalf of Y." Subject-self grants the
|
||||
> delegation; the delegatee can view + edit the draft. ARCH §4.10.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | ------------------ | ----------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_submission_id` | ULID FK | → form_submissions, cascade delete |
|
||||
| `delegated_to_user_id` | ULID FK | → users, cascade delete |
|
||||
| `delegated_by_user_id` | ULID FK | → users, cascade delete |
|
||||
| `granted_at` | timestamp | |
|
||||
| `revoked_at` | timestamp nullable | Null = delegation active |
|
||||
| `message` | text nullable | Optional context from delegator to delegatee |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Indexes:** `(delegated_to_user_id, revoked_at)`, `(form_submission_id)`
|
||||
**Soft delete:** no
|
||||
|
||||
---
|
||||
|
||||
### `form_values`
|
||||
|
||||
> EAV row per `(submission, field)`. **Integer AI PK** — this table is
|
||||
> joined heavily and integer joins beat ULID joins for the hot read
|
||||
> paths. Canonical payload lives in the `value` JSON column; the typed
|
||||
> columns are derived. ARCH §4.4.
|
||||
>
|
||||
> **Observer behaviour (`FormValueObserver`, ARCH §7.2):**
|
||||
> - When `form_fields.is_filterable = true` on a scalar field,
|
||||
> `value_indexed` is populated (truncated to 255 chars) from the
|
||||
> JSON value.
|
||||
> - When `form_fields.value_storage_hint = number`, `value_number` is
|
||||
> populated as decimal(15,4).
|
||||
> - When `value_storage_hint = date`, `value_date` is populated.
|
||||
> - When `value_storage_hint = bool`, `value_bool` is populated.
|
||||
> - When `is_filterable = false`, all typed columns are reset to NULL
|
||||
> and every matching `form_value_options` row is deleted.
|
||||
> - On filterable multi-value fields (`MULTISELECT`, `CHECKBOX_LIST`,
|
||||
> `TAG_PICKER`), the `form_value_options` pivot is rebuilt (delete
|
||||
> all, insert current) on each save.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | -------------------- | ------------------------------------------- |
|
||||
| `id` | int AI | PK |
|
||||
| `form_submission_id` | ULID FK | → form_submissions, cascade delete |
|
||||
| `form_field_id` | ULID FK | → form_fields, cascade delete |
|
||||
| `value` | JSON | Canonical payload |
|
||||
| `value_indexed` | string(255) nullable | Populated by observer for filterable scalar fields |
|
||||
| `value_number` | decimal(15,4) null | Populated when `value_storage_hint = number` |
|
||||
| `value_date` | date nullable | Populated when `value_storage_hint = date` |
|
||||
| `value_bool` | bool nullable | Populated when `value_storage_hint = bool` |
|
||||
| `value_anonymised` | bool | default: false — set by anonymisation |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Unique constraint:** `UNIQUE(form_submission_id, form_field_id)`
|
||||
**Indexes:** `(form_field_id, value_indexed)`, `(form_field_id, value_number)`, `(form_field_id, value_date)`
|
||||
**Soft delete:** no — history preserved via submission soft delete
|
||||
|
||||
---
|
||||
|
||||
### `form_value_options`
|
||||
|
||||
> Filter pivot for multi-value field types (`MULTISELECT`,
|
||||
> `CHECKBOX_LIST`, `TAG_PICKER`). Denormalises `form_field_id` and
|
||||
> `form_submission_id` for fast filtering joins. Rebuilt by the
|
||||
> observer on every save — rows have no long-term identity beyond the
|
||||
> row they belong to. ARCH §4.5.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------- | ------------ | ----------------------------------------------------- |
|
||||
| `id` | int AI | PK |
|
||||
| `form_value_id` | int FK | → form_values, cascade delete |
|
||||
| `form_field_id` | ULID FK | → form_fields, cascade delete (denormalised) |
|
||||
| `form_submission_id` | ULID FK | → form_submissions, cascade delete (denormalised) |
|
||||
| `option_value` | string(255) | Single selected option |
|
||||
|
||||
**Indexes:** `(form_field_id, option_value)`, `(form_submission_id)`, `(form_value_id)`
|
||||
**Soft delete:** no
|
||||
|
||||
---
|
||||
|
||||
### `form_templates`
|
||||
|
||||
> Org-scoped reusable schema snapshots. Applying a template creates a
|
||||
> new `form_schemas` row with fields copied from the snapshot. System
|
||||
> templates (`is_system = true`) ship with Crewli and cannot be deleted
|
||||
> — only deactivated. `OrganisationScope` applied. ARCH §4.6.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | -------------- | -------------------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `organisation_id` | ULID FK | → organisations, cascade delete |
|
||||
| `name` | string | |
|
||||
| `slug` | string | |
|
||||
| `purpose` | string(50) | `FormPurpose` enum value — constrains which schemas this template can seed |
|
||||
| `description` | text nullable | |
|
||||
| `schema_snapshot` | JSON | Canonical snapshot shape (ARCH §4.6.1) — schema metadata + sections[] + fields[] block |
|
||||
| `is_system` | bool | default: false |
|
||||
| `is_active` | bool | default: true |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Indexes:** `(organisation_id, purpose, is_active)`
|
||||
**Unique constraint:** `UNIQUE(organisation_id, slug)`
|
||||
**Global scope:** `OrganisationScope`
|
||||
**Soft delete:** no
|
||||
|
||||
---
|
||||
|
||||
### `form_schema_webhooks`
|
||||
|
||||
> Webhook subscriptions per schema. `url` and `secret` are stored as
|
||||
> encrypted TEXT (Eloquent cast on the model). API resources never
|
||||
> echo these back — only `url_host` + `has_secret`. ARCH §4.11.
|
||||
> `OrganisationScope` NOT applied directly — enforced through the
|
||||
> parent schema.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| -------------------------- | ------------ | ----------------------------------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_id` | ULID FK | → form_schemas, cascade delete |
|
||||
| `name` | string | |
|
||||
| `trigger_event` | string(40) | `submission_created` / `submission_submitted` / `submission_reviewed` / `section_submitted` / `section_approved` / `section_rejected` |
|
||||
| `url` | text | Encrypted via Eloquent cast |
|
||||
| `secret` | text nullable | Encrypted via Eloquent cast; used for HMAC-SHA256 signing |
|
||||
| `is_active` | bool | default: true |
|
||||
| `created_at`, `updated_at` | timestamps | |
|
||||
|
||||
**Indexes:** `(form_schema_id, is_active)`
|
||||
**Soft delete:** no
|
||||
|
||||
---
|
||||
|
||||
### `form_webhook_deliveries`
|
||||
|
||||
> Delivery audit + retry queue populated by `FormWebhookDispatcher` and
|
||||
> driven through by `DeliverFormWebhookJob` on the dedicated `webhooks`
|
||||
> queue. Retries: `{1m, 5m, 30m, 2h, 8h}`, max 5 attempts, status flips
|
||||
> to `dead_letter` on exhaustion. SSRF-protected. ARCH §4.12 + §17.5.
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------------------- | ------------------------- | -------------------------------------------------------------- |
|
||||
| `id` | ULID | PK |
|
||||
| `form_schema_webhook_id` | ULID FK | → form_schema_webhooks, cascade delete |
|
||||
| `form_submission_id` | ULID FK | → form_submissions, cascade delete |
|
||||
| `trigger_event` | string(40) | Mirrors the webhook's trigger |
|
||||
| `status` | string(20) | `FormWebhookDeliveryStatus` enum: `pending` / `delivered` / `failed` / `dead_letter` |
|
||||
| `attempts` | int unsigned | default: 0 |
|
||||
| `last_attempt_at` | timestamp nullable | |
|
||||
| `response_status` | smallint unsigned nullable | HTTP status code |
|
||||
| `response_body_excerpt` | text nullable | First ~1000 chars |
|
||||
| `next_retry_at` | timestamp nullable | |
|
||||
| `delivered_at` | timestamp nullable | |
|
||||
| `failed_permanently_at` | timestamp nullable | |
|
||||
| `payload_snapshot` | JSON | Exactly what was (or will be) sent — for replay/audit |
|
||||
|
||||
**Indexes:** `(status, next_retry_at)`, `(form_schema_webhook_id, status)`, `(form_submission_id)`
|
||||
**Soft delete:** no — no `created_at` / `updated_at` either; `last_attempt_at` is the effective timestamp
|
||||
|
||||
---
|
||||
|
||||
**Activity log strategy:** explicit calls via
|
||||
`FormSchema::logSchemaChange()` and `FormField::logFieldChange()` — no
|
||||
`LogsActivity` trait (would produce noise). Only impactful events
|
||||
logged (publish toggle, purpose change, binding change, `is_pii`
|
||||
toggle, etc.). Bulk-fixture suppression via
|
||||
`App\Support\ActivityLog::suppressed(fn () => …)` which flips
|
||||
`config('activitylog.enabled')` for the callback.
|
||||
|
||||
**Tag sync integration (FORM-02, ARCH §31.10):** the
|
||||
`SyncTagPickerSelectionsOnSubmit` listener fires on
|
||||
`FormSubmissionSubmitted` for `event_registration` schemas with a
|
||||
person subject. It rebuilds `user_organisation_tags` rows where
|
||||
`source = self_reported` through `FormTagSyncService::rebuildForPerson`
|
||||
— no-op when `person.user_id IS NULL` (the deferred-sync path runs
|
||||
via `PersonIdentityService::confirmMatch` once the person is linked).
|
||||
|
||||
**Multi-tenancy:** `OrganisationScope` applied on `FormSchema`, `FormTemplate`, `FormFieldLibrary`. Other form-builder tables inherit isolation through their parent schema; `FormSchemaWebhook` documents this discipline explicitly via a docblock warning to never query directly without an eager constraint.
|
||||
|
||||
Reference in New Issue
Block a user