- FormFieldBindingMergeStrategy::nullWinnerBehaviour() and
isValidForScalarTargets() encode the per-strategy null-winner matrix
(RFC Q7) and the collection-only restriction (RFC V1).
- ResolvedBinding/BindingApplicationResult/BindingPassResult readonly
DTOs for the binding pipeline. Construction-time validation for
trust level. Apply-status derived from result aggregate.
Note: the existing enum is named FormFieldBindingMergeStrategy (not
MergeStrategy as the prompt sketched). Methods added to it directly.
Refs: RFC-WS-6.md §3 (Q4, Q7), §4 (V1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DismissalReasonType has six values; manually_resolved is intentionally
absent because Resolve and Dismiss are separate workflows (RFC V2).
Refs: RFC-WS-6.md §3 (Q4 partial-status separation), §4 (V2 dismiss enum)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three byte-identical copies of `normaliseLegacyGroupShape` lived in
FormFieldService, StoreFormFieldRequest, and UpdateFormFieldRequest.
WS-5d (form_fields.options) would have been the fourth copy. Hoist
the helper to a single public static on FormFieldConditionalLogicService
and have all three call sites delegate.
Implementation:
- `FormFieldConditionalLogicService::normaliseLegacyShape(array)` —
pure recursive passthrough. Translates the ARCH §8 JSON group shape
(`{"all": [...]}` / `{"any": [...]}`) into the service's internal
`{"operator", "children"}` form. Does NOT validate; malformed shapes
return as-is and surface downstream as
`InvalidConditionalLogicSpecException` from `assertSpecsValid`.
- Group operator catalogue sourced from
`FormFieldConditionalLogicGroupOperator::values()` instead of an
`['all', 'any']` literal — single source of truth for future
operator additions.
- All three call sites switched to the static method. The two
FormRequests reach it via the existing `use` import; FormFieldService
sits in the same namespace.
Behaviour preserved exactly:
- Existing FormFieldApiTest (cyclic logic rejection),
FormFieldStrictConditionalLogicRequestTest (strict-validator
rejection paths), and FormFieldConditionalLogicServiceTest
(service-level paths) all green without modification.
New unit tests pin the passthrough contract (8 tests):
- Valid ALL / ANY translations
- Recursive nested-group translation (depth 2)
- Internal shape unchanged
- Condition leaf passthrough
- Unknown group key (`xor`) returned unchanged for downstream
`assertSpecsValid` to reject
- Empty array unchanged
- Non-array children stripped silently
Tests: 1150 → 1158 green (3110 → 3124 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- PurposeRegistryTest: all seven purposes load with expected shape;
`get()` throws PurposeNotFoundException on unknown slug;
`allSubjectTypes()` returns exactly [artist, company, person, user];
`publicAccessibleSlugs()` is only `[event_registration]`.
- PurposeSchemaLifecycleTest: data-provider-driven create → publish
for all seven purposes; negative tests for event_registration (three
missing bindings) and supplier_intake (company.name missing); partial
binding test reports only the missing subset.
- CustomPurposeEscapeRemovedTest: column gone, config file gone,
FormPurpose::CUSTOM gone, store endpoint rejects `'custom'`, resource
payload omits the field.
- SubjectTypeRegistryConsolidationTest: submission validation accepts
registry subject types, rejects everything else including the legacy
`event` alias that used to be allowed.
- MorphMapAlignmentTest: compile-time guard that every
PurposeRegistry::allSubjectTypes() alias appears in the morph-map and
in AppServiceProvider::PURPOSE_SUBJECT_FQCN.
- FormPurposeTest rewritten to cover the seven v1.0 cases and the
registry-delegation helpers (now extends Tests\TestCase for the
container).
- Public/listener tests swap the removed PUBLIC_RSVP / PUBLIC_COMPLAINT
/ FEEDBACK references for valid v1.0 purposes, preserving their
negative-path assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UserProfileTest: belongs-to user, fillable/non-fillable boundaries,
settings cast, lastSubmittedAt accessor (null + max from user-subject
submissions only, ignoring drafts and is_test rows).
FormSchemaTest: ULID PK, OrganisationScope filtering, polymorphic owner
resolution to Event, purpose enum cast, hasMany fields/submissions, and
logSchemaChange() actually creates an activity-log entry.
FormFieldTest: belongs-to schema, field_type stored as string (not DB
enum), binding/translations array casts, hasMany values, soft-delete
preserves historical values, logFieldChange() creates an entry.
FormSubmissionTest: belongs-to schema, polymorphic subject resolution,
status enum cast, schema_snapshot array cast, hasMany values.
FormValueTest: belongs-to submission/field, value array cast, hasMany
options pivot rebuilt by observer, unique-pair DB constraint enforced.
MultiTenancyTest: OrganisationScope correctly filters FormSchema /
FormTemplate / FormFieldLibrary by route-resolved organisation. Pins
the FormSchemaWebhook un-scoped behaviour explicitly so a future scope
addition is an intentional decision, not an accident.
MigrationRollbackTest (group 'slow'): full migrate:fresh → rollback 14
S1 steps → assert all 13 form-builder tables dropped + legacy tables
intentionally retained → re-migrate and assert table list matches
snapshot. Plus a separate test exercising the populate-user-profiles
migration's down().
Supporting tweaks:
- UserProfile::lastSubmittedAt accessor now returns Carbon|null instead
of a raw timestamp string — testable, and matches Eloquent convention.
- UserProfileFactory cooperates with UserObserver via newModel override
(updates the auto-created row instead of inserting a duplicate).
- AppServiceProvider morph map extended with all 12 form-builder model
keys so logSchemaChange/logFieldChange resolve under enforceMorphMap.
Suite: 945 passed (was 911), 2671 assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FormValueObserver: value_indexed is filter-driven per ARCH §4.4, not
hint-driven. Populating it for every string-hint field produced dead
weight in the partial index and made FilterQueryBuilder logic murkier.
Behaviour after fix:
hint=string, is_filterable=true → populate value_indexed
hint=string, is_filterable=false → leave null
hint=number/date/bool, any filterable → populate typed column (unchanged)
hint=json, any filterable → leave typed columns null (unchanged)
value_number / value_date / value_bool remain hint-driven — they serve
display and sorting beyond filtering. Only value_indexed is gated.
VerifyFormsDataIntegrity: "value_indexed set on non-filterable field"
is now a FAIL (was WARN) — it means the observer didn't run correctly,
which is a real integrity issue.
Observer tests: split the old "string hint populates value_indexed"
case into filterable/non-filterable pair. Full suite 911/911.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
9 backed string enums covering purpose, field type, submission status/mode/review,
field width, value storage hint, snapshot mode, webhook delivery status.
FormPurpose/FormFieldType include helper methods per ARCH §3/§5. All with
declare(strict_types=1) and values() helpers for validation rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three verification methods (TOTP authenticator, email code, backup codes),
trusted device management with 30-day expiry, role-based enforcement for
super_admin and org_admin, admin reset capability, and full test coverage
(46 tests). Modifies login flow to support MFA challenge/response with
temporary session tokens stored in cache.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>