Commit Graph

15 Commits

Author SHA1 Message Date
c5b0210ae7 feat(form-builder): add FormSubmissionActionFailure model + apply_status casts (WS-6)
- FormSubmissionActionFailure: audit model, no organisation_id (FK-chain
  tenancy per RFC V3), open/resolved/dismissed scopes, canBeRetried()
  helper. Morph alias 'form_submission_action_failure' registered for
  future activity-log subject references.
- FormSubmission: apply_status (ApplyStatus enum cast),
  apply_completed_at (datetime), actionFailures() HasMany,
  scopePendingApply().

Refs: RFC-WS-6.md §3 (Q5), §4 (V3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:06 +02:00
0dd991c688 feat(form-builder): add BindingTypeRegistry as single source of truth for target shapes (WS-6)
Config-driven mapping from (target_entity, target_attribute) to storage
shape (scalar/collection/relation), PHP type, and identity-key
eligibility. Replaces any name-suffix matching (e.g. _tags, _skills) —
those are convention-not-contract and reject by design.

Used by publish guards now and (in session 2) by FormBindingApplicator.

Refs: RFC-WS-6.md §4 (V1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:41:25 +02:00
b2e9ef8824 feat(form-builder): MergeStrategy enum methods + binding value objects (WS-6)
- 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>
2026-04-25 22:38:55 +02:00
447511634d feat(form-builder): add ApplyStatus, DismissalReasonType, BindingTargetType enums (WS-6)
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>
2026-04-25 22:36:10 +02:00
2656818c35 refactor(form-field): extract legacy conditional_logic shape normaliser
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>
2026-04-25 00:57:06 +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
55ba4f24c0 test(form-builder): cover purpose registry and morph-map alignment
- 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>
2026-04-24 14:36:09 +02:00
cd7a804024 test(forms): model tests, multi-tenancy, migration rollback (Phase 9)
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>
2026-04-17 16:44:47 +02:00
ccdfd5b77b fix(forms): gate value_indexed population on is_filterable
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>
2026-04-17 15:28:15 +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
135bdb352c feat(forms): add PHP enums for form builder
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>
2026-04-17 11:27:01 +02:00
948687f27e feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices
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>
2026-04-15 20:45:55 +02:00
34e12e00b3 feat: initial commit - Band Management application 2026-01-06 03:11:46 +01:00